Production packaging for the single-node Community Edition: three multi-stage images, one Compose stack, Caddy edge with automatic HTTPS.
- Stack definition:
infra/compose/production.yml - Edge proxy config:
infra/caddy/Caddyfile - Images:
docker/Dockerfile.api,docker/Dockerfile.web,docker/Dockerfile.gateway - Environment template:
.env.example(commented production block at the bottom)
- Linux host, x86_64 or arm64 (Raspberry Pi 5 works for light rooms)
- Docker Engine 24+ with the compose plugin
- 2+ CPUs and 4 GB RAM minimum; see sizing table below
- Ports 8080/8443 free for local trials, or 80/443 for a public domain
git clone https://github.com/<owner>/home-ctf-box.git openrange
cd openrange
cp .env.example .envEdit .env. Uncomment the production block at the bottom of the file, then fill every secret. Values marked openssl rand -hex 32 must be generated, not left blank:
openssl rand -hex 32 # repeat per secret: SECRET_KEY, FLAG_MASTER_KEY,
# RUNNER_API_TOKEN, GATEWAY_SECRET, MEILI_MASTER_KEY,
# POSTGRES_PASSWORD, S3 keysThe typed validator (packages/config) fails closed in production mode:
missing secrets or leftover dev-only placeholders stop startup instead of
serving traffic.
Bring up databases and caches first, apply schema, then start everything:
docker compose -f infra/compose/production.yml --env-file .env up -d postgres redis minio meilisearch
docker compose -f infra/compose/production.yml --env-file .env run --rm migrate
docker compose -f infra/compose/production.yml --env-file .env up -d --wait--wait returns once every healthcheck passes. The API reports readiness at
/api/readyz (database, Redis, content dir, migration head); the gateway at
/healthz; the web container answers on /.
Smoke test through the Caddy edge (loopback default):
curl -fsS http://127.0.0.1:8080/api/readyz | jq .
curl -fsS http://127.0.0.1:8080/healthz
curl -fsSI http://127.0.0.1:8080/ | head -n1Create the first admin through the setup wizard at
http://<host>:8080/setup; no default credentials ship in any image.
Pin upgrades to immutable tags; never run latest unattended.
# 1. Backup first (Postgres dump + content archive + manifest)
bash scripts/operations/backup.sh backups/pre-upgrade-$(date -u +%Y%m%d)
# 2. Pull new images
sed -i 's/^OPENRANGE_.*_IMAGE=.*/&/' .env # set *_IMAGE to the new vX.Y.Z
docker compose -f infra/compose/production.yml --env-file .env pull
# 3. Apply migrations via the one-shot container, then roll services
docker compose -f infra/compose/production.yml --env-file .env up -d postgres redis minio meilisearch
docker compose -f infra/compose/production.yml --env-file .env run --rm migrate
docker compose -f infra/compose/production.yml --env-file .env up -d --waitRepeat the readyz smoke test after each upgrade.
Images are immutable digests; rollback is a tag flip plus restart:
# point OPENRANGE_*_IMAGE back at the previous vX.Y.Z in .env
docker compose -f infra/compose/production.yml --env-file .env up -d --force-recreate --waitDatabase caveat: migrations are forward-only here. If a release migrated the schema, restore the pre-upgrade backup instead of downgrading in place:
docker compose -f infra/compose/production.yml --env-file .env down
bash scripts/operations/restore.sh backups/<stamp>
# then start the old image set and re-run the smoke testWith OPENRANGE_DOMAIN= empty the stack serves plain HTTP on loopback,
which is right for trials and for running behind your own reverse proxy.
For public installs: create an A/AAAA record for range.example.com
pointing at the host, then set in .env:
OPENRANGE_DOMAIN=range.example.com
CADDY_HTTP_BIND=0.0.0.0:80
CADDY_HTTPS_BIND=0.0.0.0:443
OPENRANGE_PUBLIC_URL=https://range.example.com
Recreate with docker compose -f infra/compose/production.yml --env-file .env up -d caddy.
Caddy obtains and renews the Let's Encrypt certificate automatically;
certificates and ACME state persist in the caddy-data volume.
Lab instances themselves must never be exposed publicly; only this control
plane edge is. Lab traffic leaves through the gateway route /gateway/*,
and room containers stay on internal networks (see docs/decisions/
0009-deployment-boundaries.md).
| Profile | CPU | RAM | Fits |
|---|---|---|---|
| Minimum | 2 vCPU | 4 GB | Control plane + a few light web rooms |
| VPS | 4 vCPU | 8 GB | Small classes, concurrent browser labs |
| Recommended | 8 vCPU | 16 GB | Full single-node Community Edition |
Per-service limits are declared in production.yml under
deploy.resources.limits; scale them together with host capacity.
Windows/AD content and multi-host ranges need external x86_64 runners
registered against the lab runner API instead of the Pi-class host.
- Content updates: edit
content/, thendocker compose -f infra/compose/production.yml --env-file .env restart api; content is mounted read-only from the checkout, not baked into images. - Logs rotate at 10 MB x 3 per service (json-file).
- Backups:
scripts/operations/backup.sh(cron it), verify withscripts/operations/verify-backup.sh, restore withscripts/operations/restore.sh. - Release automation lives in
.github/workflows/release.yml: pushing tagvX.Y.Zbuilds all three images into GHCR and attaches syft SPDX SBOMs plus a changelog to the GitHub Release.