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.
This commit is contained in:
+50
-13
@@ -2,8 +2,10 @@ services:
|
||||
db:
|
||||
image: docker.io/library/postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
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
|
||||
@@ -13,16 +15,39 @@ services:
|
||||
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: ["sh", "-c", "alembic upgrade head && exec fintracker serve --host 0.0.0.0 --port 8000"]
|
||||
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:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- uploads:/data/uploads
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
expose:
|
||||
- "8000"
|
||||
|
||||
@@ -31,16 +56,28 @@ services:
|
||||
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:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- uploads:/data/uploads
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
|
||||
caddy:
|
||||
image: docker.io/library/caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
DOMAIN: ${DOMAIN:?set DOMAIN in .env}
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -55,7 +92,6 @@ services:
|
||||
pg-backup:
|
||||
image: docker.io/library/postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
entrypoint: ["sh", "-c"]
|
||||
# nightly logical dump, keep 14 days; copy ./backups off-site with rclone/cron on the host
|
||||
command:
|
||||
@@ -67,6 +103,8 @@ services:
|
||||
sleep 86400
|
||||
done
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
PGPASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- ./backups:/backups
|
||||
@@ -76,6 +114,5 @@ services:
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
uploads:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
|
||||
Reference in New Issue
Block a user