services: db: image: docker.io/library/postgres:17-alpine restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_INITDB_ARGS: "--encoding=UTF8 --no-locale" volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s timeout: 3s retries: 20 # one-shot: api and worker start only after the schema is at head, so neither races the other migrate: build: ./backend environment: DATABASE_URL: ${DATABASE_URL} command: ["alembic", "upgrade", "head"] depends_on: db: condition: service_healthy api: build: ./backend restart: unless-stopped env_file: .env command: ["fintracker", "serve", "--host", "0.0.0.0", "--port", "8000"] # not curl: the slim image has none. An empty ProxyHandler because a host proxy baked into # the image at build time (see docs/ai/ops.md) would otherwise swallow 127.0.0.1. healthcheck: test: - CMD - python - -c - >- import sys, urllib.request as u; o = u.build_opener(u.ProxyHandler({})); sys.exit(0 if o.open("http://127.0.0.1:8000/api/v1/health", timeout=4).status == 200 else 1) interval: 30s timeout: 6s retries: 3 start_period: 30s depends_on: migrate: condition: service_completed_successfully expose: - "8000" worker: build: ./backend restart: unless-stopped env_file: .env command: ["fintracker", "worker"] # the worker has no port; it touches a file every 10 s (`worker/scheduler.py: _heartbeat`) healthcheck: test: - CMD - python - -c - >- import os, sys, time; sys.exit(0 if time.time() - os.path.getmtime("/tmp/fintracker-worker-heartbeat") < 60 else 1) interval: 30s timeout: 5s retries: 3 start_period: 60s depends_on: migrate: condition: service_completed_successfully caddy: image: docker.io/library/caddy:2-alpine restart: unless-stopped environment: DOMAIN: ${DOMAIN:?set DOMAIN in .env} ports: - "80:80" - "443:443" volumes: - ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro - ./app/build/web:/srv/app:ro - caddy_data:/data - caddy_config:/config depends_on: - api pg-backup: image: docker.io/library/postgres:17-alpine restart: unless-stopped entrypoint: ["sh", "-c"] # nightly logical dump, keep 14 days; copy ./backups off-site with rclone/cron on the host command: - | while true; do ts=$(date +%F_%H%M) pg_dump -h db -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -Fc -f "/backups/fintracker_$$ts.dump" && \ find /backups -name 'fintracker_*.dump' -mtime +14 -delete sleep 86400 done environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} PGPASSWORD: ${POSTGRES_PASSWORD} volumes: - ./backups:/backups depends_on: db: condition: service_healthy volumes: pgdata: caddy_data: caddy_config: