haico-backend, haico-frontend), pushes them to the GitHub
Container Registry (GHCR), and runs them on two self-hosted environments via docker compose. The
deployment trees live under deployment/: dev/ and prod/ are structurally identical (they pull
images), while local/ builds from source (see Local installation).
Release model: two branches, tag-driven
The flow (recorded in ADR-0001) has two long-lived branches:develop integrates all PRs and deploys to dev; main is stable, only ever fast-forwarded from
develop, and deploys to production. A release is cut by pushing a SemVer tag.
Tags must be strict SemVer; anything else is rejected by the pipeline’s
resolve-release job. The
GitHub Environment name matches the deployment/<env>/ directory name one-to-one.
Promotion to
main is a local git merge --ff-only + push, not the GitHub merge button. On a
free private repo “Require linear history” cannot be enforced, and a UI merge would add a merge
commit, breaking the byte-for-byte equality between the dev-vX.Y.Z and vX.Y.Z images. See
CONTRIBUTING.md and the Maintainers handbook.The CI/CD pipeline
One workflow,.github/workflows/ci.yml, drives everything. On a SemVer tag push it runs:
The deploy job connects to the target host over OpenVPN, SSHes in, renders the root / backend
/ frontend .env files by name-matching each key against the environment’s GitHub secrets, copies
the deployment/<env>/ tree over, does a docker login ghcr.io, and runs the host’s scripts/deploy.sh
(which pulls the pinned images, runs docker compose up -d, and health-checks Postgres, the backend,
and the frontend). The frontend image bakes the per-environment NEXT_PUBLIC_* and API_PROXY_TARGET
build args, so it is built once per channel.
Runtime topology
A single reverse-proxy host runs nginx (on the host, not in a container) and fronts both environments, terminating TLS and routing by domain to each app host’s Compose stack:/ to the frontend, /api/ and /health to the backend, and /phoenix/ to
Phoenix. The committed nginx configs are deployment/prod/nginx/ and deployment/dev/nginx/; they depend on
host-resident snippets and rate-limit zones that live only on the proxy host.
Required GitHub Environment secrets
Configure GitHub Environmentsdev and prod (Settings → Environments) with the same secret
names; add a required reviewer to prod to gate production auto-deploy.
The image refs (
BACKEND_IMAGE / FRONTEND_IMAGE) are not secrets; the deploy job computes them and
pins the exact X.Y.Z for reproducibility.
Cutting a release
1
Dev
/promote dev on a PR: promote-dev.yml auto-bumps and pushes the next
dev-vX.Y.Z onto the PR head (it uses a PAT so the tag triggers CI).2
Production
BACKEND_IMAGE / FRONTEND_IMAGE in ~/haico_deploy/.env then
run scripts/deploy.sh. Migrations are forward-only, so snapshot the database before a migration
release.