# 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=5", "--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