gendesign/docker-compose.prod.yml
lekss361 94cf199276
Some checks failed
Deploy Trade-In / changes (push) Successful in 5s
Deploy / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Successful in 26s
Deploy Trade-In / build-backend (push) Successful in 47s
Deploy / build-frontend (push) Successful in 29s
Deploy / build-backend (push) Successful in 1m24s
Deploy Trade-In / deploy (push) Successful in 40s
Deploy / build-worker (push) Successful in 2m57s
Deploy / deploy (push) Failing after 37s
feat(tradein): postgres_fdw live read of gendesign.cad_buildings (replaces snapshot) (#493)
Replaces tradein.cad_buildings snapshot with live postgres_fdw foreign table reading gendesign.v_tradein_cad_buildings. Fixes /trade-in/api/v1/geocode/reverse 500 (Nominatim ban) and address_not_geocoded for cadastre addresses (e.g. Хохрякова 81).

Security (deep-review fixes):
- 100_tradein_fdw_role.sql: passwordless CREATE ROLE; password set by deploy.yml ALTER ROLE bootstrap reading GENDESIGN_FDW_PASSWORD from backend/.env.runtime (via psql :'pw' var → format %L — injection-safe).
- core/fdw.py: regex whitelist [A-Za-z0-9_-]{32,256} on password, ValueError without echoing value, try/rollback on commit.
- 060_postgres_fdw_extension.sql: connect_timeout='3' on FOREIGN SERVER + ALTER ADD/SET fallback.
- geocoder.py: _cadastral_forward_sync / _cadastral_reverse_sync wrapped in asyncio.to_thread.
- 100_*.sql: REVOKE ALL ON ALL TABLES/SEQUENCES/FUNCTIONS IN SCHEMA public; only GRANT SELECT on v_tradein_cad_buildings.
- pg_user_mappings query handles PUBLIC mapping (usename IS NULL).

Tests: 3 SQL-injection guards on ensure_fdw_user_mapping + rewritten cadastral suite.
2026-05-24 08:57:30 +00:00

247 lines
9.1 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
networks:
default: {}
shared:
aliases:
- gendesign-postgres
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_healthy
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
# TCP-only probe через node (есть в alpine image, запускает сам server.js).
# НЕ использует HTTP semantics — проверяет только что порт 3000 слушает.
# Если Next.js процесс жив и порт открыт → healthy, независимо от HTTP status.
# start_period 60s даёт время Next.js standalone bundle bootstrap.
healthcheck:
test: ["CMD", "node", "-e", "require('net').createConnection({port:3000,host:'127.0.0.1'}).once('connect',function(){process.exit(0)}).once('error',function(){process.exit(1)})"]
interval: 15s
timeout: 5s
retries: 6
start_period: 60s
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_healthy
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_healthy
# --schedule=/tmp/...: default location `/app/celerybeat-schedule` падает
# с Permission denied — WORKDIR `/app` принадлежит root, `app` (uid 1000)
# не может создавать там файлы. `/tmp` всегда writable; schedule-файл
# хранит только last_run_at для periodic tasks — потеря на restart OK,
# beat перестроит из `celery_app.conf.beat_schedule` на старте.
command: ["celery", "-A", "app.workers.celery_app", "beat", "--loglevel=info", "--schedule=/tmp/celerybeat-schedule"]
glitchtip-web:
image: glitchtip/glitchtip:6.1.6
container_name: glitchtip-web
# profiles: ["glitchtip"] keeps this service from starting on plain `compose up -d`.
# Bootstrap script activates the profile after DB + secrets are ready.
# On subsequent deploys, set COMPOSE_PROFILES=glitchtip in /opt/gendesign/.env.
profiles: ["glitchtip"]
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
environment:
DATABASE_URL: postgres://glitchtip:${GLITCHTIP_DB_PASS}@postgres:5432/glitchtip
REDIS_URL: redis://redis:6379/2
SECRET_KEY: ${GLITCHTIP_SECRET}
PORT: "8080"
EMAIL_URL: consolemail://
GLITCHTIP_DOMAIN: https://errors.gendsgn.ru
DEFAULT_FROM_EMAIL: errors@gendsgn.ru
ENABLE_USER_REGISTRATION: "true"
ENABLE_ORGANIZATION_CREATION: "false"
restart: always
mem_limit: 512m
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/api/0/"]
interval: 30s
timeout: 5s
retries: 5
start_period: 60s
networks: [default]
glitchtip-worker:
image: glitchtip/glitchtip:6.1.6
container_name: glitchtip-worker
profiles: ["glitchtip"]
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
command: ./bin/run-celery-with-beat.sh
environment:
DATABASE_URL: postgres://glitchtip:${GLITCHTIP_DB_PASS}@postgres:5432/glitchtip
REDIS_URL: redis://redis:6379/2
SECRET_KEY: ${GLITCHTIP_SECRET}
CELERY_WORKER_AUTOSCALE: "1,3"
restart: always
mem_limit: 384m
networks: [default]
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/users.caddy.snippet:/etc/caddy/caddy/users.caddy.snippet:ro
- ./preview:/srv/preview:ro
- caddy_data:/data
- caddy_config:/config
- caddy_logs:/var/log/caddy
# backend: service_healthy — backend имеет /health endpoint, готов до Caddy.
# frontend: service_started — НЕ service_healthy: даже если frontend healthcheck
# дребезжит, Caddy всё равно стартует (можно отдавать 502 для /, но
# obsidian.gendsgn.ru, api.gendsgn.ru и т.д. остаются доступны).
depends_on:
backend:
condition: service_healthy
frontend:
condition: service_started
networks:
- default
# shared — для маршрута obsidian.gendsgn.ru → couchdb (отдельный stack).
# Если obsidian-стек не задеплоен, Caddy просто получит 502 на этом маршруте,
# main-приложение не страдает.
- shared
glitchtip-auth-forwarder:
# Собирается локально на VPS при деплое (не тянется из GHCR).
# deploy.yml запускает: docker compose build glitchtip-auth-forwarder
build: ./ops/glitchtip-auth-forwarder
container_name: gendesign-auth-forwarder
restart: unless-stopped
environment:
GLITCHTIP_DSN: ${GLITCHTIP_DSN}
APP_ENV: production
APP_RELEASE: auth-forwarder-1
CADDY_LOG_FILE: /var/log/caddy/auth_audit.log
STATE_FILE: /state/offset.json
volumes:
# Read-only доступ к Caddy access log
- caddy_logs:/var/log/caddy:ro
# Persistent offset — выживает при перезапуске контейнера
- auth_forwarder_state:/state
depends_on:
- caddy
volumes:
postgres_data:
redis_data:
caddy_data:
caddy_config:
caddy_logs:
auth_forwarder_state:
networks:
# Внешняя сеть, создаётся вне compose (см. docs/obsidian-livesync.md).
# Связывает main-stack (Caddy) и obsidian-stack (CouchDB).
shared:
external: true
name: gendesign_shared