gendesign/docker-compose.prod.yml
lekss361 5c92c2a0e9 fix(worker): auto-resume after redeploy + bump concurrency 5->8
Auto-resume bug: kn and nspd_geo resume-on-worker_ready required
heartbeat >5min / >10min stale. After redeploy worker boots in
~1-2 min, so jobs killed seconds before deploy had fresh heartbeat
and were NEVER auto-resumed — required manual cancel + resume.
Fix: drop time threshold entirely. On worker_ready ANY 'running'
or 'paused' job is by definition a zombie (no worker exists yet),
safe to resume all of them.

Concurrency bump: 5 -> 8 prefork slots. Headroom for 5 geo jobs +
1 kn sweep + 2 objective tasks running simultaneously. Each slot
~150MB RSS -> ~1.2GB total, well within 6GB VPS RAM budget.
2026-05-11 16:23:16 +03:00

145 lines
4.8 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Production compose. Pulls pre-built images from GHCR
# (built and pushed by .github/workflows/deploy.yml).
#
# Place at /opt/gendesign/docker-compose.prod.yml on the VM.
# Required env in /opt/gendesign/.env:
# POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD
# IMAGE_TAG (defaults to "latest")
#
# In production the browser talks to Caddy on :80/:443.
# Caddy proxies /api/* and /health straight to backend, the rest to frontend.
# So the frontend uses same-origin relative URLs — no NEXT_PUBLIC_API_BASE_URL needed.
#
# Postgres + Redis run alongside the app on the same VM (Discovery mode).
# Volumes are shared with docker-compose.yml so switching between files preserves data.
services:
postgres:
image: postgis/postgis:16-3.4
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
# Bind to 127.0.0.1 only — accessible from host (for SSH tunnel) but not from public internet.
# UFW additionally blocks 5432 from outside.
ports:
- "127.0.0.1:5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./backend/db/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
backend:
image: ghcr.io/lekss361/gendesign-backend:${IMAGE_TAG:-latest}
restart: unless-stopped
# .env.runtime пишется deploy.yml через SSH (SENTRY_RELEASE=$IMAGE_TAG).
# required: false — compose не падает если файла нет (первый деплой).
env_file:
- path: ./backend/.env
- path: ./backend/.env.runtime
required: false
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
ports:
- "127.0.0.1:8000:8000"
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 5
start_period: 20s
frontend:
image: ghcr.io/lekss361/gendesign-frontend:${IMAGE_TAG:-latest}
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
depends_on:
- backend
worker:
# Отдельный chromium-образ (+200 МБ Playwright). См. backend/Dockerfile target=runner-with-chromium.
image: ghcr.io/lekss361/gendesign-worker:${IMAGE_TAG:-latest}
restart: unless-stopped
env_file:
- path: ./backend/.env
- path: ./backend/.env.runtime
required: false
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
volumes:
- ./data:/app/data # playwright_state.json + photo binaries
# Read-only bind-mount Антоновского /sf/ SQLite — для objective_etl task.
# На VPS файл лежит в /opt/gendesign/site-finder/analysis.db; путь внутри
# контейнера задан settings.objective_anton_sqlite_path (default
# /data/anton-sqlite/analysis.db).
- /opt/gendesign/site-finder:/data/anton-sqlite:ro
command: ["celery", "-A", "app.workers.celery_app", "worker", "--loglevel=info", "--concurrency=8", "--queues=celery,scrape_kn,geo"]
beat:
# Lean backend-образ (без Chromium) — beat только триггерит таски в Redis.
image: ghcr.io/lekss361/gendesign-backend:${IMAGE_TAG:-latest}
restart: unless-stopped
env_file:
- path: ./backend/.env
- path: ./backend/.env.runtime
required: false
depends_on:
redis:
condition: service_started
command: ["celery", "-A", "app.workers.celery_app", "beat", "--loglevel=info"]
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- backend
- frontend
networks:
- default
# shared — для маршрута obsidian.gendsgn.ru → couchdb (отдельный stack).
# Если obsidian-стек не задеплоен, Caddy просто получит 502 на этом маршруте,
# main-приложение не страдает.
- shared
volumes:
postgres_data:
redis_data:
caddy_data:
caddy_config:
networks:
# Внешняя сеть, создаётся вне compose (см. docs/obsidian-livesync.md).
# Связывает main-stack (Caddy) и obsidian-stack (CouchDB).
shared:
external: true
name: gendesign_shared