gendesign/tradein-mvp/docker-compose.prod.yml
lekss361 5fb5675871
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Successful in 35s
Deploy Trade-In / build-frontend (push) Successful in 38s
Deploy / build-worker (push) Successful in 42s
Deploy / build-backend (push) Successful in 43s
Deploy / build-frontend (push) Successful in 42s
Deploy / deploy (push) Successful in 1m32s
Deploy Trade-In / test (push) Successful in 1m50s
Deploy Trade-In / build-backend (push) Successful in 52s
Deploy Trade-In / deploy (push) Successful in 51s
fix(tradein/security): defense-in-depth trusted-header auth + rate-limiter fix (#2213) (#2229)
2026-07-02 21:15:58 +00:00

171 lines
8.7 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.

# Trade-In MVP — production overlay для деплоя ВНУТРИ gendesign-стека.
#
# Запускается из /opt/gendesign/tradein-mvp/ на bot-server:
# docker compose -p gendesign \
# -f docker-compose.yml -f docker-compose.prod.yml up -d
#
# Особенности vs локального docker-compose.yml:
# - НЕТ своего Caddy и postgres-данных в bind mount
# (postgres контейнер один на всю машину, но изолированная БД tradein_mvp)
# - Образы тянутся из GHCR (build делает GitHub Actions)
# - Backend/frontend подключены к network gendesign_shared чтобы основной
# Caddy мог проксировать /trade-in/* → tradein-frontend
# - Frontend строится с basePath=/trade-in (см. next.config.ts)
services:
browser:
image: ghcr.io/lekss361/gendesign-tradein-browser:${IMAGE_TAG:-latest}
container_name: tradein-browser
# 5g: N параллельных headless-Firefox страниц (BROWSER_CONCURRENCY, default 4)
# — каждая ~0.8-1.5GB. VPS имеет ~6.9G свободно. Было 2.5g под 1-за-раз (#884/#905).
mem_limit: 5g
env_file:
- path: ./backend/.env.runtime
required: false
environment:
BROWSER_PORT: "3000"
# Сколько /fetch параллельно. Дефолт 4; ops поднимет до 8 в .env.runtime после смоука.
BROWSER_CONCURRENCY: ${BROWSER_CONCURRENCY:-4}
# SCRAPER_PROXY_URL читается из .env.runtime (см. env_file выше)
expose:
- "3000"
restart: unless-stopped
networks:
- tradein-net
postgres:
image: postgis/postgis:16-3.4
container_name: tradein-postgres
environment:
POSTGRES_DB: tradein
POSTGRES_USER: ${TRADEIN_POSTGRES_USER:-tradein}
POSTGRES_PASSWORD: ${TRADEIN_POSTGRES_PASSWORD:?required}
volumes:
- tradein-postgres-data:/var/lib/postgresql/data
- ./backend/data/sql:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${TRADEIN_POSTGRES_USER:-tradein}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- tradein-net
- gendesign_shared
backend:
image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest}
container_name: tradein-backend
# Prod: uvicorn БЕЗ --reload (Dockerfile CMD несёт --reload только для dev hot-reload,
# где app/ bind-mount'ится). В prod --reload = лишний WatchFiles-наблюдатель + риск
# reload в середине запроса. Single worker сохраняется для предсказуемости
# (один process = единственный asyncio event loop, нет shared-state race).
# Scheduler переехал в tradein-scraper (#1182) — SCHEDULER_ENABLE=false ниже.
command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# .env.runtime пишется вручную на VPS (см. DEPLOY.md / scripts/README.md).
# Контейнер читает YANDEX_GEOCODER_API_KEY, COOKIE_ENCRYPTION_KEY и пр. из
# этого файла — без него backfill scripts падают с "key required".
# required: false — compose не падает если файла нет (первый деплой).
env_file:
- path: ./backend/.env.runtime
required: false
environment:
DATABASE_URL: "postgresql+psycopg://${TRADEIN_POSTGRES_USER:-tradein}:${TRADEIN_POSTGRES_PASSWORD}@postgres:5432/tradein"
PUBLIC_URL: "https://gendsgn.ru/trade-in"
CORS_ORIGINS: '["https://gendsgn.ru"]'
ENVIRONMENT: "production"
CONTACT_EMAIL: "${TRADEIN_CONTACT_EMAIL:-tradein@gendsgn.ru}"
# Yandex Geocoder API key — читается из backend/.env.runtime (env_file).
# Раньше дублировалось здесь как ${YANDEX_GEOCODER_API_KEY:-} — убрано
# т.к. environment: overrides env_file и при пустой host-env стирает значение.
# GlitchTip DSN — мониторинг ошибок (#396). Пусто = выключено.
GLITCHTIP_DSN: "${GLITCHTIP_DSN:-}"
GENDESIGN_FDW_PASSWORD: "${GENDESIGN_FDW_PASSWORD:-}"
# Encryption key for Cian session cookies (pgp_sym_encrypt). Required for
# Valuation Calculator (Stage 9). Empty = Calculator scraper disabled +
# cookies upload endpoint returns 503. Generate with `openssl rand -hex 32`.
COOKIE_ENCRYPTION_KEY: "${COOKIE_ENCRYPTION_KEY:-}"
# Scheduler отключён в API-процессе — он живёт в tradein-scraper (#1182).
# Это предотвращает двойной запуск sweep'ов при наличии обоих контейнеров.
SCHEDULER_ENABLE: "false"
# #2213 defense-in-depth: общий секрет с Caddy. ЗАДАН → backend требует
# валидный X-Internal-Auth-Secret на каждом запросе с X-Authenticated-User
# (иначе 401). Пусто = защита не активна (fail-open до провижининга).
# Значение ДОЛЖНО совпадать с TRADEIN_INTERNAL_AUTH_SECRET в .env главного
# (Caddy) стека. Читается также из backend/.env.runtime (env_file выше).
TRADEIN_INTERNAL_AUTH_SECRET: "${TRADEIN_INTERNAL_AUTH_SECRET:-}"
depends_on:
browser:
condition: service_started
postgres:
condition: service_healthy
volumes:
# RBAC roles config — single source of truth (../auth/roles.yaml в репо,
# это /opt/gendesign/auth/roles.yaml на VPS). MIRROR of main backend mount.
# app/core/auth.py читает /app/auth/roles.yaml для middleware + /me.
- ../auth/roles.yaml:/app/auth/roles.yaml:ro
restart: unless-stopped
networks:
- tradein-net
- gendesign_shared # чтобы Caddy gendesign-стека достучался
# Отдельный scraper-контейнер (#1182): scheduler_loop() живёт здесь, а не в API.
# Мотивация: docker restart tradein-backend убивал бегущие sweep'ы (avito/cian/
# rosreestr могут идти часами). Теперь backend-деплой не трогает scraper.
# Образ тот же что у backend (один Dockerfile), команда другая.
# НЕ пересоздаётся при каждом backend-деплое — селективный up в deploy-tradein.yml
# поднимает scraper только при изменениях scraper/infra путей (см. SCRAPER_CHANGED).
scraper:
image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest}
container_name: tradein-scraper
command: ["python", "-m", "app.scheduler_main"]
env_file:
- path: ./backend/.env.runtime
required: false
environment:
DATABASE_URL: "postgresql+psycopg://${TRADEIN_POSTGRES_USER:-tradein}:${TRADEIN_POSTGRES_PASSWORD}@postgres:5432/tradein"
ENVIRONMENT: "production"
GLITCHTIP_DSN: "${GLITCHTIP_DSN:-}"
GENDESIGN_FDW_PASSWORD: "${GENDESIGN_FDW_PASSWORD:-}"
COOKIE_ENCRYPTION_KEY: "${COOKIE_ENCRYPTION_KEY:-}"
SCHEDULER_ENABLE: "true"
depends_on:
postgres:
condition: service_healthy
browser:
condition: service_started
restart: unless-stopped
# Phase 1: give the in-flight scrape unit (browser card ~15-27s) time to finish
# + checkpoint on SIGTERM before Docker SIGKILLs. Default grace is only 10s, which
# SIGKILLs a running card mid-flight. (Cooperative-drain handler lands in a later phase.)
stop_grace_period: 120s
stop_signal: SIGTERM
networks:
- tradein-net
frontend:
image: ghcr.io/lekss361/gendesign-tradein-frontend:${IMAGE_TAG:-latest}
container_name: tradein-frontend
environment:
NODE_ENV: production
# Next.js standalone server.js читает HOSTNAME для bind-адреса.
# Docker по умолчанию ставит HOSTNAME=<container-id> → сервер слушает
# только свой hostname-интерфейс и недоступен как tradein-frontend:3000.
# Принудительно 0.0.0.0 чтобы Caddy достучался.
HOSTNAME: "0.0.0.0"
BACKEND_URL: "http://backend:8000" # internal SSR
depends_on: [backend]
restart: unless-stopped
networks:
- tradein-net
- gendesign_shared
volumes:
tradein-postgres-data:
name: tradein-postgres-data
networks:
tradein-net:
name: tradein-net
gendesign_shared:
external: true