gitea-saas-core/docker-compose.yml
Claude 5be23b9390 gitea-saas-core#40: container + ops doc (Dockerfile, compose, OPS.md, .dockerignore)
- Dockerfile: python:3.12-slim, non-root app user, gunicorn wsgi:application,
  HEALTHCHECK on the public /health route.
- Single-writer ledger safety: exactly ONE gunicorn worker (SQLite money
  ledger; BillingCore's lock serialises writes in-process; threads are safe,
  multiple writer processes are not - stated in the Dockerfile and OPS.md).
- docker-compose.yml: named volume saas_data as the durability story;
  SAAS_API_KEY defaults to empty -> fail-closed 503, /health stays public.
- OPS.md: start/stop, online backup (sqlite3 backup API), restore procedure
  (incl. clearing WAL sidecars), key rotation, down-checklist, scaling notes.
- .dockerignore: runtime files only.

Test evidence (local on branch issue-40; no VPS deploy under this ticket):
- python -m pytest -q: 273 passed, 1 skipped.
- docker compose up --build: /health 200, container health=healthy.
- Persistence: client created + credited 2500 -> down/up -> read back intact.
- Restore drill: backup -> drift client B -> restore -> integrity_check ok,
  client A (balance 2500) present, client B absent (404).

Closes #40 once merged + deployed; the VPS run is a separate owner-approved step.
2026-09-05 04:18:12 -05:00

42 lines
1.5 KiB
YAML

# gitea-saas-core deployment (issue #40).
#
# The MEANING OF THE VOLUME (read before changing it):
# The service stores its money ledger in SQLite at /data/saas.db. The
# named volume `saas_data` is therefore the ENTIRE durability story:
# losing the volume loses the ledger. `docker compose down` keeps the
# volume. `docker compose down -v` DESTROYS it — never run that without
# a verified backup (OPS.md "Backup"/"Restore"). Backups before traffic.
#
# SQLite is a single-writer store: the container runs exactly one gunicorn
# worker (see Dockerfile) so all writes funnel through one process lock.
# Do not raise --workers here.
name: gitea-saas
services:
saas:
build: .
image: gitea-saas-core:local
restart: unless-stopped
ports:
- "8080:8080"
environment:
# Empty by default on purpose: with no key the app still boots and
# /health stays public, but every authed route fails closed with 503
# (see gitea_saas/app.py). Set a real key before serving traffic:
# docker compose --env-file .env up -d
# with SAAS_API_KEY=<secret> in .env, or export SAAS_API_KEY first.
SAAS_API_KEY: ${SAAS_API_KEY:-}
SAAS_DB_PATH: ${SAAS_DB_PATH:-/data/saas.db}
SAAS_PORT: "8080"
volumes:
- saas_data:/data
healthcheck:
test: ["CMD", "python", "-c", "import sys,urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=5).status == 200 else 1)"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
volumes:
saas_data: