Files
padelnomics/docker-compose.prod.yml
Deeman c0c8607664 fix: migration atomicity + deploy hardening + Litestream R2
Migration atomicity:
- Remove conn.commit() and executescript() from all up() functions (0000,
  0011, 0012, 0013, 0014, 0015); executescript() issued implicit COMMITs
  which broke the batch-rollback guarantee of the migration runner
- Rewrite 0000 with individual conn.execute() calls (was a single
  executescript block)

Deploy hardening:
- Add pre-migration DB backup step to deploy.sh: saves
  app.db.pre-deploy-<timestamp> in the volume before every migration
- On health-check failure: restore the backup, then stop + exit
- On success: clean up old backups (keep last 3)

Litestream:
- Enable R2 as primary replica in litestream.yml (env-var placeholders)
- Add local /app/data/backups as secondary replica
- docker-compose: add auto-restore on empty volume (sh entrypoint runs
  'litestream restore' before 'litestream replicate' if app.db missing)
- Add LITESTREAM_R2_* vars to .gitlab-ci.yml .env block and .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 10:28:59 +01:00

141 lines
3.6 KiB
YAML

services:
# ── Always-on infrastructure ──────────────────────────────
router:
image: nginx:alpine
restart: unless-stopped
ports:
- "5000:80"
volumes:
- ./router/default.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- net
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 5s
litestream:
image: litestream/litestream:latest
restart: unless-stopped
# Auto-restore from R2 if DB file is missing, then start continuous replication.
# Handles: new server, deleted volume, disaster recovery.
entrypoint: /bin/sh
command:
- -c
- |
if [ ! -f /app/data/app.db ]; then
echo "==> No database found, restoring from R2..."
litestream restore -config /etc/litestream.yml /app/data/app.db \
|| echo "==> No backup found, starting fresh"
fi
exec litestream replicate -config /etc/litestream.yml
env_file: ./padelnomics/.env
volumes:
- app-data:/app/data
- ./padelnomics/litestream.yml:/etc/litestream.yml:ro
# ── Blue slot ─────────────────────────────────────────────
blue-app:
profiles: ["blue"]
build:
context: ./padelnomics
restart: unless-stopped
env_file: ./padelnomics/.env
environment:
- DATABASE_PATH=/app/data/app.db
volumes:
- app-data:/app/data
networks:
- net
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
blue-worker:
profiles: ["blue"]
build:
context: ./padelnomics
restart: unless-stopped
command: python -m padelnomics.worker
env_file: ./padelnomics/.env
environment:
- DATABASE_PATH=/app/data/app.db
volumes:
- app-data:/app/data
networks:
- net
blue-scheduler:
profiles: ["blue"]
build:
context: ./padelnomics
restart: unless-stopped
command: python -m padelnomics.worker scheduler
env_file: ./padelnomics/.env
environment:
- DATABASE_PATH=/app/data/app.db
volumes:
- app-data:/app/data
networks:
- net
# ── Green slot ────────────────────────────────────────────
green-app:
profiles: ["green"]
build:
context: ./padelnomics
restart: unless-stopped
env_file: ./padelnomics/.env
environment:
- DATABASE_PATH=/app/data/app.db
volumes:
- app-data:/app/data
networks:
- net
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
green-worker:
profiles: ["green"]
build:
context: ./padelnomics
restart: unless-stopped
command: python -m padelnomics.worker
env_file: ./padelnomics/.env
environment:
- DATABASE_PATH=/app/data/app.db
volumes:
- app-data:/app/data
networks:
- net
green-scheduler:
profiles: ["green"]
build:
context: ./padelnomics
restart: unless-stopped
command: python -m padelnomics.worker scheduler
env_file: ./padelnomics/.env
environment:
- DATABASE_PATH=/app/data/app.db
volumes:
- app-data:/app/data
networks:
- net
volumes:
app-data:
networks:
net: