Files
fin-tracker/docker-compose.yml
T
Dmitry 4a0b4e0532 feat(deploy): healthcheck api и worker, отдельный migrate-сервис, лимит загрузки отчёта
Миграции вынесены из команды api в one-shot сервис migrate, api и worker стартуют после него. /health отвечает 503 при недоступной БД. Отчёт больше MAX_UPLOAD_BYTES (25 МиБ) получает 413, Caddy режет на 30 МБ раньше. Том uploads убран, секреты env_file передаются сервисам явно. В CI добавлена сборка образа бэкенда без push, test_migrations сверяет модели с историей Alembic.
2026-09-19 21:55:33 +03:00

119 lines
3.2 KiB
YAML

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: