Deployment¶
The platform is deployed with Docker Compose behind an edge nginx. The stack: PostgreSQL 16 (pgvector) + Redis 7 + the Go API + the Next.js BFF.
Host specifics are not on this page
Server addresses, paths and credentials live in the in-repository
deploy/HOST.md runbook — this site is public, so it carries none of them.
Topology¶
Internet ──► edge nginx (80/443, Let's Encrypt)
│ conf.d/geregeapp.mn.conf ← owned by this repository
▼
gerege-app-web (Next.js BFF)
│ BACKEND_URL
▼
internal compose network (no public ports): api ──► db + redis
The edge nginx is a separate compose project. The app's web container
joins it through a shared docker network.
The vhost ownership model¶
Many ecosystem domains are served by one shared edge nginx whose conf.d
directory is mounted from another repository's working copy — and that
repository's deploy runs git reset --hard.
git reset --hard only restores tracked files; it leaves untracked files
alone. So this repository installs its own vhost there as an untracked
file and fully owns its configuration.
What the script does:
- Finds the host path automatically from the edge container's
/etc/nginx/conf.dmount. - Backs up the previous version.
- Installs the new file (preserving directory ownership,
0644). - Runs
nginx -t— on failure it restores the previous version and does not reload. - Runs
nginx -s reload.
| Env | Default |
|---|---|
EDGE_CONTAINER |
gerege-nginx |
EDGE_CONF_D |
Auto-detected from the container mount |
SKIP_EDGE |
Set to 1 for deploy.sh to skip it |
Env files (gitignored)¶
| File | What | Ownership |
|---|---|---|
.env |
Compose interpolation (Postgres/Redis secrets, ports, origin) | root, 0600 on the host |
backend.env |
API configuration (JWT, eID, SSO, OIDC…) | uid 65532, 0600 |
Ownership of backend.env
The api and migrate containers run as distroless nonroot (uid
65532). backend.env is mounted at /app/.env, so a root-owned 0600
file is unreadable — migrate fails immediately with
failed to load config file.
Never share secrets between deployments
Every deployment has its own JWT_SECRET, SSO_STATE_KEY,
INTEGRATION_ENC_KEY and RP credentials. Generate them on the host with
openssl rand — never through git, CI or chat.
Deploying¶
cd <repo path>
git fetch --prune origin && git reset --hard origin/main
bash deploy/deploy.sh # build → up → wait for healthy → install edge
Properties of deploy.sh:
- No-op guard — if nothing changed it does nothing at all (
.deployed-sha). - Migration as its own step — this removed the 502 that used to accompany every deploy.
- It does not rebuild
db.
CI/CD¶
Pushing to main runs .github/workflows/ci.yml:
| Job | What |
|---|---|
| Backend | gofmt -l . empty · go vet · go test -race · build |
| Frontend | npm run lint · npm run test · npm run build |
| Secrets | gitleaks |
| Deploy | After all three pass, SSH in, git reset --hard <sha> and run deploy/deploy.sh |
Required repo secrets: DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY,
DEPLOY_PATH. The CI SSH key is separate from any operator key.
TLS¶
Let's Encrypt in webroot mode. Certificates live in an edge volume; a cron job attempts renewal twice a day and reloads nginx on success.
The vhost has its own listen 80 block (ACME challenge plus HTTPS redirect), so
it does not depend on the shared default server and renewal works independently.
Diagnostics¶
docker compose ps
docker compose logs api --tail 50
docker compose logs migrate --tail 20 # "migration [up] success"
docker exec gerege-nginx nginx -t # config validity
curl -s -o /dev/null -w '%{http_code}\n' https://geregeapp.mn/
| Symptom | Cause | Fix |
|---|---|---|
444 / dropped connection |
The vhost file was deleted (git clean -fd) |
bash deploy/edge/install.sh |
502 Bad Gateway |
The web container is down or not on the edge network |
docker compose up -d; check the override file |
conflicting server name |
A duplicate vhost in the shared config | Remove it there — this repo owns the name |
| nginx will not start | Missing certificate | Obtain one with certbot |
migrate failed to load config |
Ownership of backend.env |
chown 65532:65532 |
The host compose override¶
docker-compose.override.yml — which attaches the web container to the edge
network and gives it a container_name — lives only on the host
(untracked).
- Tracked in the repository, it would force production settings onto every developer's machine.
- Not gitignored,
git clean -fdwould delete it and the container would leave the edge network, giving the vhost a 502.
Restoring it:
This documentation site¶
The documentation is a separate static site served under
docs.gerege.mn/app/:
Internet ──► gerege-nginx (docs.gerege.mn vhost)
├─ / ─► docs-gerege-web (ecosystem documentation)
└─ /app/ ─► gerege-app-docs-web (this documentation)
Full runbook: docs-site/deploy/README.md in the repository.