feat(docker): CYCLONE_ADMIN_* env vars for backend

This commit is contained in:
Nora
2026-06-22 15:21:55 -06:00
parent 4402a993d1
commit ca40fd72c3
2 changed files with 118 additions and 4 deletions
+101
View File
@@ -0,0 +1,101 @@
# Cyclone — local docker-compose stack.
#
# Two services on a user-defined network:
#
# frontend (nginx) — published on http://127.0.0.1:8080
# serves the built SPA + reverse-proxies /api/* to backend
# backend (uvicorn) — NOT published externally by default; reachable from
# the frontend over the compose network on backend:8000
# (override `ports:` below to expose it for curl/debug)
#
# Persistent state:
# - `cyclone-data` named volume mounted at /data in the backend holds
# the SQLite database file. Survives `docker compose down`; only
# `docker compose down -v` wipes it.
# - `config/payers.yaml` is baked into the backend image at build time.
# To edit payer config, change the YAML, rebuild the backend image,
# and `POST /api/admin/reload-config` (or just restart the container).
#
# Usage:
# docker compose up -d --build # start (or rebuild + restart)
# docker compose logs -f # tail both services
# docker compose restart backend # bounce the backend (e.g. after crash)
# docker compose down # stop containers, KEEP the data volume
# docker compose down -v # stop AND wipe the data volume
name: cyclone
services:
backend:
build:
context: .
dockerfile: backend/Dockerfile
image: cyclone-backend:local
container_name: cyclone-backend
restart: unless-stopped
environment:
# Bind on all interfaces inside the container (required — 127.0.0.1
# would only be reachable from inside the container itself).
CYCLONE_HOST: "0.0.0.0"
CYCLONE_PORT: "8000"
CYCLONE_RELOAD: "0"
# Absolute path inside the container; the named volume mounts at /data.
CYCLONE_DB_URL: "sqlite:////data/cyclone.db"
# Bootstrap admin (required on first boot unless at least one user
# already exists). The ${VAR:?msg} syntax makes docker-compose refuse
# to start with a clear error if the env var isn't set in the host
# environment.
CYCLONE_ADMIN_USERNAME: ${CYCLONE_ADMIN_USERNAME:?CYCLONE_ADMIN_USERNAME is required on first boot}
CYCLONE_ADMIN_PASSWORD: ${CYCLONE_ADMIN_PASSWORD:?CYCLONE_ADMIN_PASSWORD is required on first boot (min 12 chars)}
volumes:
- cyclone-data:/data
# The healthcheck in the Dockerfile hits /api/health. The frontend
# depends_on `service_healthy` so it won't accept traffic until the
# backend is responsive.
healthcheck:
test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8000/api/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
# No `ports:` — the frontend reaches the backend over the compose
# network. Uncomment the next line to expose the API directly for
# `curl http://localhost:8000/api/...` from the host.
# ports:
# - "127.0.0.1:8000:8000"
networks:
- cyclone-net
frontend:
build:
context: .
dockerfile: frontend/Dockerfile
image: cyclone-frontend:local
container_name: cyclone-frontend
restart: unless-stopped
depends_on:
backend:
condition: service_healthy
ports:
# Bind address defaults to 0.0.0.0 (reachable from the LAN). Set
# CYCLONE_BIND_ADDRESS=127.0.0.1 to tighten to loopback-only and
# match the standalone install's local-only posture.
# Port defaults to 8081 to dodge the common clash on 8080; override
# with CYCLONE_WEB_PORT=... (see README).
- "${CYCLONE_BIND_ADDRESS:-0.0.0.0}:${CYCLONE_WEB_PORT:-8081}:80"
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1/healthz"]
interval: 15s
timeout: 3s
retries: 3
start_period: 5s
networks:
- cyclone-net
networks:
cyclone-net:
driver: bridge
volumes:
cyclone-data:
name: cyclone-data