Skip to main content
HAI-Co² builds two Docker images (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:
The proxy forwards / 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 Environments dev 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.
Phoenix tracing. With auth on, the OTLP collector is protected too, so the backend needs a Phoenix System API key in PHOENIX_API_KEY, or trace exports fail with UNAUTHENTICATED. And PHOENIX_SECRET shorter than 32 characters crash-loops the container.

Cutting a release

1

Dev

Or comment /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

Rollback: tags are immutable, so re-tag a new SemVer at the previous good commit and let CI redeploy, or SSH to the host and pin 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.