Template for small Go + React projects that should be easy to deploy on a VPS with Docker Compose.
The backend and frontend are ready to run. The infrastructure directory contains the VPS deploy stack.
backend: idiomatic Go HTTP API, SQLite, JSON logs, Prometheus metrics, basic OpenTelemetry tracing.frontend: your React app.infrastructure: Docker Compose, Caddy, encrypted env workflow, bootstrap/deploy/smoke scripts, Prometheus, Grafana, Loki, and Tempo.
cd backend
go run ./cmd/serverPrepare encrypted prod env locally first:
./infrastructure/scripts/local-bootstrap.sh prod
./infrastructure/scripts/env.sh generate-key prodEdit local plaintext env files:
infrastructure/env/prod/stack.env
infrastructure/env/prod/frontend.env
infrastructure/env/prod/backend.env
Then encrypt and clean plaintext:
./infrastructure/scripts/env.sh encrypt prod
./infrastructure/scripts/env.sh clean prod
git add infrastructure/env/prod/*.env.age infrastructure/env/prod/recipients.txtKeep identity.txt out of git and store it in your password manager or secure deploy storage.
On a fresh Ubuntu server, install the required tools and firewall rules:
./infrastructure/scripts/server-bootstrap.shThen point DNS records for APP_DOMAIN and MONITOR_DOMAIN to the server, copy the private age identity securely, and run:
AGE_IDENTITY_FILE=/secure/path/prod.agekey ./infrastructure/scripts/env.sh decrypt prod
./infrastructure/scripts/deploy.sh prod
./infrastructure/scripts/smoke-test.sh prodCaddy handles HTTPS automatically when the DNS records are correct and ports 80 and 443 are open.
GitHub Actions includes:
ci.yml: Go format/test/vet, frontend lint/build, shell syntax, Docker Compose config validation, and deploy image builds.deploy-prod.yml: aftercisucceeds onmainor via manual dispatch, syncs the repo to the VPS over SSH, decrypts prod env files withage, runs deploy, then runs smoke tests.
Create these repository secrets before using production deploy:
PROD_HOST: server IP or hostname.PROD_USER: deploy user on the server.PROD_SSH_KEY: private SSH key forPROD_USER.PROD_AGE_IDENTITY_B64: base64 ofinfrastructure/env/prod/identity.txt.
Optional secrets:
PROD_PORT: SSH port, defaults to22.PROD_DEPLOY_PATH: server path, defaults to/home/<PROD_USER>/fx.PROD_SMOKE_IP: smoke-test target IP, defaults to127.0.0.1.
Create the age identity secret value locally:
base64 -w0 infrastructure/env/prod/identity.txtRun this once on a fresh Ubuntu server before the first deploy:
./infrastructure/scripts/server-bootstrap.sh