Central production deploys from tested container images, not from source code on the server.
CI in .github/workflows/ci.yml owns release creation:
- Pull requests run validation and build disposable GHCR images tagged
pr-<number>-<sha12>. - Pushes to
mainrun validation, build GHCR images taggedsha-<sha12>, and smoke-test that exact image set with the production Compose file. - Git tags matching
v*.*.*also run validation, buildsha-<sha12>images, and smoke-test the image set. - After smoke tests pass on a tag push, CI publishes version tags from the tested SHA images.
- Exact stable tags such as
v1.2.3also move thestabletag. - Prerelease tags such as
v1.3.0-rc.1publish only that version tag and do not movestable. - Tag releases package a deploy bundle artifact containing:
docker-compose.prod.ymlcentral-update.env.prod.example
The release tag must point at a commit reachable from main; CI rejects release tags outside main.
Production Compose pulls these images from GHCR:
ghcr.io/themattcode/central/app-cockpit:${CENTRAL_VERSION}ghcr.io/themattcode/central/service-backend:${CENTRAL_VERSION}ghcr.io/themattcode/central/i12e-postgres:${CENTRAL_VERSION}ghcr.io/themattcode/central/i12e-gateway:${CENTRAL_VERSION}
CENTRAL_VERSION defaults to stable. Use exact release tags for pinned deployments and rollbacks.
From a clean main commit:
git tag v1.2.3
git push origin v1.2.3Then wait for CI to pass. The tag push publishes the versioned image set, updates stable for exact stable SemVer tags, and uploads the deploy bundle artifact.
For a prerelease:
git tag v1.3.0-rc.1
git push origin v1.3.0-rc.1Deploy prereleases by exact version. stable remains unchanged.
Install Docker and Tailscale on the production host. Tailscale is host-managed; the Compose stack binds the gateway to 127.0.0.1:4000 by default so Tailscale Serve can expose it over the tailnet.
Unpack the deploy bundle on the server, then create the environment file once:
cp .env.prod.example .env.prodSet production values in .env.prod:
POSTGRES_PASSWORDBACKEND_DATABASE_URLBACKEND_CORS_ALLOW_ORIGINCENTRAL_ORIGIN- optional
CENTRAL_VERSION
If GHCR packages are private, log in on the host:
docker login ghcr.ioRun from the unpacked deploy bundle:
./central-updateThe script prompts for a version and defaults to stable.
Deploy an exact version:
./central-update v1.2.3Deploy a prerelease:
./central-update v1.3.0-rc.1Major version jumps require explicit approval:
./central-update v2.0.0 --allow-majorThe update script:
- writes
CENTRAL_VERSIONto.env.prod, - pulls the selected image set,
- starts PostgreSQL and waits for health,
- creates a PostgreSQL backup for major jumps or when
--backupis passed, - runs migrations,
- starts
service-backend,app-cockpit, andi12e-gateway, - checks gateway and backend health,
- prints Compose status.
Backups are written to backups/ next to the deploy bundle unless BACKUP_DIR overrides it. Use --skip-backup only when an external backup already exists.
Rollback by deploying an older exact release tag:
./central-update v1.2.2Database rollback is not automatic. If migrations are not backward-compatible, restore from a backup before starting the older version.
Use the same env and Compose file as the update script:
docker compose --env-file .env.prod --file docker-compose.prod.yml ps
docker compose --env-file .env.prod --file docker-compose.prod.yml logsStop the stack:
docker compose --env-file .env.prod --file docker-compose.prod.yml down