feat(sp23): docker-compose.yml with backend + frontend, named volumes, secrets

This commit is contained in:
Cyclone
2026-06-23 17:22:06 -06:00
parent 67dae61a94
commit d42fbc8c1b
2 changed files with 88 additions and 78 deletions
+13
View File
@@ -0,0 +1,13 @@
# Repo-root .dockerignore — applies to `docker compose build` (which builds
# both backend and frontend contexts from the repo root). Excludes anything
# that should never end up in a build context.
.worktrees/
.git/
.github/
docs/prodfiles/
*.production.txt
node_modules/
dist/
.venv/
.superpowers/brainstorm/
+75 -78
View File
@@ -1,101 +1,98 @@
# 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 name: cyclone
# Two-container production stack for Cyclone.
#
# backend FastAPI on python:3.11-slim-bookworm, internal port 8000.
# frontend nginx:1.27-alpine serving the built React SPA + reverse-
# proxying /api/* to the backend, host port 8080.
#
# Tag strategy:
# - Images are tagged semver from package.json / pyproject.toml.
# - Two aliases per image: `:latest` (set automatically by `docker
# compose build`) and `:stable` (manually promoted after a release
# has been running in prod for >=1 week).
# - Compose defaults to `:stable` so a fresh `docker compose build`
# never auto-rolls a new release.
# - Override with `TAG=0.0.9 docker compose up -d` to roll back.
services: services:
backend: backend:
image: cyclone-backend:${TAG:-stable}
build: build:
context: . context: ./backend
dockerfile: backend/Dockerfile
image: cyclone-backend:local
container_name: cyclone-backend
restart: unless-stopped restart: unless-stopped
environment: environment:
# Bind on all interfaces inside the container (required — 127.0.0.1 # SQLite path lives on the named `cyclone_db` volume.
# would only be reachable from inside the container itself). CYCLONE_DB_URL: "sqlite:////var/lib/cyclone/db/cyclone.db"
CYCLONE_HOST: "0.0.0.0" # Wire the existing BackupService (SP17) to autostart on container boot.
CYCLONE_PORT: "8000" CYCLONE_BACKUP_AUTOSTART: "1"
CYCLONE_RELOAD: "0" CYCLONE_BACKUP_INTERVAL_HOURS: "24"
# Absolute path inside the container; the named volume mounts at /data. CYCLONE_BACKUP_RETENTION_DAYS: "14"
CYCLONE_DB_URL: "sqlite:////data/cyclone.db" # Logging — JSON to stdout (collected by `docker logs`) and to the
# Bootstrap admin (required on first boot unless at least one user # bind-mounted file (collected by host-side logrotate).
# already exists). The ${VAR:?msg} syntax makes docker-compose refuse CYCLONE_LOG_LEVEL: "INFO"
# to start with a clear error if the env var isn't set in the host CYCLONE_LOG_FILE: "/var/log/cyclone/cyclone.log"
# environment. CYCLONE_LOG_JSON: "1"
CYCLONE_ADMIN_USERNAME: ${CYCLONE_ADMIN_USERNAME:?CYCLONE_ADMIN_USERNAME is required on first boot} # Cookie signing key defaults to the SQLCipher key. Override in
CYCLONE_ADMIN_PASSWORD: ${CYCLONE_ADMIN_PASSWORD:?CYCLONE_ADMIN_PASSWORD is required on first boot (min 12 chars)} # production if you want to rotate them independently.
CYCLONE_SECRET_KEY_FILE: "/run/secrets/cyclone_db_key"
CYCLONE_COOKIE_SECURE: "1"
# First-admin bootstrap — populated by scripts/cyclone-init.sh.
# The `_FILE` variants are the standard Docker-secret pattern:
# the env var points at the mounted secret file rather than
# embedding the secret in the compose file.
CYCLONE_ADMIN_USERNAME_FILE: "/run/secrets/cyclone_admin_username"
CYCLONE_ADMIN_PASSWORD_FILE: "/run/secrets/cyclone_admin_password"
secrets:
- cyclone_db_key
- cyclone_admin_username
- cyclone_admin_password
volumes: volumes:
- cyclone-data:/data - cyclone_db:/var/lib/cyclone/db
# The healthcheck in the Dockerfile hits /api/health. The frontend - cyclone_backups:/var/lib/cyclone/backups
# depends_on `service_healthy` so it won't accept traffic until the - cyclone_prodfiles:/var/lib/cyclone/prodfiles
# backend is responsive. - cyclone_sftp_staging:/var/lib/cyclone/sftp_staging
- cyclone_logs:/var/log/cyclone
healthcheck: healthcheck:
test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8000/api/health"] test: ["CMD", "curl", "-fs", "http://localhost:8000/api/healthz"]
interval: 15s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
start_period: 10s start_period: 30s
# 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: networks:
- cyclone-net - cyclone_network
frontend: frontend:
image: cyclone-frontend:${TAG:-stable}
build: build:
context: . context: ./frontend
dockerfile: frontend/Dockerfile
image: cyclone-frontend:local
container_name: cyclone-frontend
restart: unless-stopped restart: unless-stopped
ports:
# LAN-bind: only this port is published to the host. The operator
# is expected to firewall this to the LAN subnet (or rely on VPN
# for outside access). NO public TLS in v1.
- "8080:8080"
depends_on: depends_on:
backend: backend:
condition: service_healthy 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: networks:
- cyclone-net - cyclone_network
networks: secrets:
cyclone-net: cyclone_db_key:
driver: bridge file: /etc/cyclone/secrets/db.key
cyclone_admin_username:
file: /etc/cyclone/secrets/admin_username
cyclone_admin_password:
file: /etc/cyclone/secrets/admin_pw
volumes: volumes:
cyclone-data: cyclone_db:
name: cyclone-data cyclone_backups:
cyclone_prodfiles:
cyclone_sftp_staging:
cyclone_logs:
networks:
cyclone_network:
driver: bridge