469 lines
16 KiB
YAML
469 lines
16 KiB
YAML
# «Контракт-чек» — infrastructure (Step 1).
|
|
#
|
|
# Default (`docker compose up`) starts ONLY infra: postgres + redis + rabbitmq
|
|
# + minio (+ minio-init). Service containers (api, worker-extract, worker-analyze,
|
|
# bot) and the observability/edge stacks are added behind profiles in later steps
|
|
# (docs/ARCHITECTURE.md §20).
|
|
#
|
|
# Durability posture (§10): quorum-ready. Postgres is configured
|
|
# wal_level=replica + WAL archiving (replica/PITR-ready). RabbitMQ quorum queues
|
|
# are declared by the app (core/mq/topology.py) — they replicate the moment a
|
|
# 3-node cluster is added. Named volumes everywhere; restart: unless-stopped.
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:18-alpine
|
|
container_name: contract_check-postgres
|
|
restart: unless-stopped
|
|
command:
|
|
- "postgres"
|
|
- "-c"
|
|
- "wal_level=replica"
|
|
- "-c"
|
|
- "archive_mode=on"
|
|
- "-c"
|
|
- "archive_command=test ! -f /walarchive/%f && cp %p /walarchive/%f"
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:-contract_check}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-contract_check}
|
|
POSTGRES_DB: ${POSTGRES_DB:-contract_check}
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql
|
|
- pgwal:/walarchive
|
|
ports:
|
|
- "15432:5432"
|
|
healthcheck:
|
|
test:
|
|
- CMD-SHELL
|
|
- "pg_isready -U ${POSTGRES_USER:-contract_check} -d ${POSTGRES_DB:-contract_check}"
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
redis:
|
|
image: redis:8-alpine
|
|
container_name: contract_check-redis
|
|
restart: unless-stopped
|
|
command: ["redis-server", "--appendonly", "yes"]
|
|
volumes:
|
|
- redisdata:/data
|
|
ports:
|
|
- "17379:6379"
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
rabbitmq:
|
|
image: rabbitmq:4-management-alpine
|
|
container_name: contract_check-rabbitmq
|
|
restart: unless-stopped
|
|
environment:
|
|
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-contract_check}
|
|
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:-contract_check}
|
|
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-/}
|
|
volumes:
|
|
- rabbitmq:/var/lib/rabbitmq
|
|
ports:
|
|
- "5672:5672" # AMQP
|
|
- "15672:15672" # management UI (http://localhost:15672)
|
|
healthcheck:
|
|
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 15s
|
|
|
|
minio:
|
|
image: minio/minio:latest
|
|
container_name: contract_check-minio
|
|
restart: unless-stopped
|
|
command: server /data --console-address ":9001"
|
|
environment:
|
|
MINIO_ROOT_USER: ${S3_ACCESS_KEY:-contract_check}
|
|
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY:-contract_check}
|
|
volumes:
|
|
- minio:/data
|
|
ports:
|
|
- "9000:9000" # S3 API
|
|
- "9001:9001" # console (http://localhost:9001)
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-sf", "http://localhost:9000/minio/health/ready"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 10s
|
|
|
|
# One-shot: create the bucket + an ILM expiry rule (152-ФЗ retention lever).
|
|
# Service containers (Step 2+) gate on `service_completed_successfully`.
|
|
minio-init:
|
|
image: minio/mc:latest
|
|
container_name: contract_check-minio-init
|
|
depends_on:
|
|
minio:
|
|
condition: service_healthy
|
|
entrypoint: /bin/sh
|
|
command:
|
|
- -c
|
|
- |
|
|
set -e
|
|
mc alias set local http://minio:9000 "$${MINIO_ROOT_USER:-contract_check}" "$${MINIO_ROOT_PASSWORD:-contract_check}"
|
|
mc mb --ignore-existing local/${S3_BUCKET:-contract-check-docs}
|
|
mc anonymous set none local/${S3_BUCKET:-contract-check-docs} || true
|
|
# Expire raw docs + extracted text after DOC_RETENTION_DAYS (default 7).
|
|
mc ilm rule add --expire-days ${DOC_RETENTION_DAYS:-7} local/${S3_BUCKET:-contract-check-docs} || true
|
|
echo "bucket ${S3_BUCKET:-contract-check-docs} ready (ilm expire ${DOC_RETENTION_DAYS:-7}d)"
|
|
environment:
|
|
MINIO_ROOT_USER: ${S3_ACCESS_KEY:-contract_check}
|
|
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY:-contract_check}
|
|
restart: "no"
|
|
|
|
# ── SERVICES (profile: services) ────────────────────────────────────────────
|
|
# Core FastAPI service. Runs migrations separately (see deploy docs); assumes
|
|
# the DB is migrated before accepting traffic via healthcheck delay.
|
|
api:
|
|
profiles: ["services"]
|
|
build:
|
|
context: .
|
|
dockerfile: srv/api/Dockerfile
|
|
container_name: contract_check-api
|
|
restart: unless-stopped
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
rabbitmq:
|
|
condition: service_healthy
|
|
minio-init:
|
|
condition: service_completed_successfully
|
|
# All app config flows from .env (12-factor; see .env.example for the full
|
|
# list). New settings need NO compose changes — pydantic Settings reads
|
|
# them with code-level defaults. `environment:` below only overrides the
|
|
# values that must point at in-compose hostnames instead of localhost.
|
|
env_file:
|
|
- path: .env
|
|
required: false
|
|
environment:
|
|
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check}
|
|
REDIS_URL: redis://redis:6379/0
|
|
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
|
|
S3_ENDPOINT_URL: http://minio:9000
|
|
OTEL_SERVICE_NAME: api
|
|
ports:
|
|
- "${API_PORT:-8000}:8000"
|
|
- "${API_METRICS_PORT:-9100}:9100"
|
|
healthcheck:
|
|
test:
|
|
- CMD-SHELL
|
|
- "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')\""
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 10
|
|
start_period: 15s
|
|
|
|
worker-extract:
|
|
profiles: ["services"]
|
|
build:
|
|
context: .
|
|
dockerfile: srv/worker-extract/Dockerfile
|
|
container_name: contract_check-worker-extract
|
|
restart: unless-stopped
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
rabbitmq:
|
|
condition: service_healthy
|
|
minio-init:
|
|
condition: service_completed_successfully
|
|
# App config flows from .env (12-factor; see .env.example). New settings
|
|
# need NO compose changes — pydantic Settings reads them with code-level
|
|
# defaults. `environment:` below only overrides the values that must point
|
|
# at in-compose hostnames instead of localhost.
|
|
env_file:
|
|
- path: .env
|
|
required: false
|
|
environment:
|
|
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check}
|
|
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
|
|
S3_ENDPOINT_URL: http://minio:9000
|
|
OTEL_SERVICE_NAME: worker-extract
|
|
ports:
|
|
- "9101:9101"
|
|
|
|
worker-analyze:
|
|
profiles: ["services"]
|
|
build:
|
|
context: .
|
|
dockerfile: srv/worker-analyze/Dockerfile
|
|
container_name: contract_check-worker-analyze
|
|
restart: unless-stopped
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
rabbitmq:
|
|
condition: service_healthy
|
|
minio-init:
|
|
condition: service_completed_successfully
|
|
# App config flows from .env (12-factor; see .env.example). New settings
|
|
# need NO compose changes — pydantic Settings reads them with code-level
|
|
# defaults. `environment:` below only overrides the values that must point
|
|
# at in-compose hostnames instead of localhost.
|
|
env_file:
|
|
- path: .env
|
|
required: false
|
|
environment:
|
|
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check}
|
|
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
|
|
S3_ENDPOINT_URL: http://minio:9000
|
|
OTEL_SERVICE_NAME: worker-analyze
|
|
ports:
|
|
- "9102:9102"
|
|
|
|
worker-prescreen:
|
|
profiles: ["services"]
|
|
build:
|
|
context: .
|
|
dockerfile: srv/worker-prescreen/Dockerfile
|
|
container_name: contract_check-worker-prescreen
|
|
restart: unless-stopped
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
rabbitmq:
|
|
condition: service_healthy
|
|
minio-init:
|
|
condition: service_completed_successfully
|
|
# App config flows from .env (12-factor; see .env.example). New settings
|
|
# need NO compose changes — pydantic Settings reads them with code-level
|
|
# defaults. `environment:` below only overrides the values that must point
|
|
# at in-compose hostnames instead of localhost.
|
|
env_file:
|
|
- path: .env
|
|
required: false
|
|
environment:
|
|
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check}
|
|
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
|
|
S3_ENDPOINT_URL: http://minio:9000
|
|
OTEL_SERVICE_NAME: worker-prescreen
|
|
ports:
|
|
- "9104:9104"
|
|
|
|
worker-billing:
|
|
profiles: ["services"]
|
|
build:
|
|
context: .
|
|
dockerfile: srv/worker-billing/Dockerfile
|
|
container_name: contract_check-worker-billing
|
|
restart: unless-stopped
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
minio-init:
|
|
condition: service_completed_successfully
|
|
env_file:
|
|
- path: .env
|
|
required: false
|
|
environment:
|
|
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check}
|
|
OTEL_SERVICE_NAME: worker-billing
|
|
ports:
|
|
- "9105:9105"
|
|
|
|
worker-notify:
|
|
profiles: ["services"]
|
|
build:
|
|
context: .
|
|
dockerfile: srv/worker-notify/Dockerfile
|
|
container_name: contract_check-worker-notify
|
|
restart: unless-stopped
|
|
depends_on:
|
|
rabbitmq:
|
|
condition: service_healthy
|
|
# App config flows from .env (12-factor; see .env.example). New settings
|
|
# need NO compose changes — pydantic Settings reads them with code-level
|
|
# defaults. `environment:` below only overrides the values that must point
|
|
# at in-compose hostnames instead of localhost.
|
|
env_file:
|
|
- path: .env
|
|
required: false
|
|
environment:
|
|
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-contract_check}:${POSTGRES_PASSWORD:-contract_check}@postgres:5432/${POSTGRES_DB:-contract_check}
|
|
RABBITMQ_URL: amqp://${RABBITMQ_USER:-contract_check}:${RABBITMQ_PASS:-contract_check}@rabbitmq:5672/${RABBITMQ_VHOST:-/}
|
|
S3_ENDPOINT_URL: http://minio:9000
|
|
OTEL_SERVICE_NAME: worker-notify
|
|
ports:
|
|
- "9103:9103"
|
|
|
|
# Telegram bot adapter (aiogram 3, HTTP-only to api). Per docs/ARCHITECTURE.md §17
|
|
# the bot holds no DB/MQ/S3 credentials — it depends on `api` being healthy,
|
|
# not on the infra containers directly, enforcing the hexagonal boundary even
|
|
# in dependency ordering.
|
|
bot:
|
|
profiles: ["services"]
|
|
build:
|
|
context: .
|
|
dockerfile: srv/bot/Dockerfile
|
|
container_name: contract_check-bot
|
|
restart: unless-stopped
|
|
depends_on:
|
|
api:
|
|
condition: service_healthy
|
|
environment:
|
|
ENV: ${ENV:-dev}
|
|
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
|
BOT_TOKEN: ${BOT_TOKEN:-}
|
|
API_URL: ${API_URL:-http://api:8000}
|
|
BOT_SERVICE_TOKEN: ${BOT_SERVICE_TOKEN:-}
|
|
|
|
# ── EDGE (profile: edge) ──────────────────────────────────────────────────
|
|
# Reverse proxy + TLS terminator. Listens on 80/443 and forwards
|
|
# /api/v1/*, /admin/*, /metrics, /healthz, /readyz to the api service.
|
|
nginx:
|
|
profiles: ["edge"]
|
|
# The conf template is BAKED into this image (deploy/nginx/Dockerfile), so
|
|
# template edits → new image → `up -d --build` recreates and re-renders it.
|
|
# The VPS override (deploy/vps/) swaps in contract-check-http.conf.template
|
|
# via a bind mount at the same target path.
|
|
build:
|
|
context: ./deploy/nginx
|
|
dockerfile: Dockerfile
|
|
container_name: contract_check-nginx
|
|
restart: unless-stopped
|
|
depends_on:
|
|
api:
|
|
condition: service_healthy
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
volumes:
|
|
- certbot-data:/etc/letsencrypt:ro
|
|
- certbot-webroot:/var/www/certbot:ro
|
|
environment:
|
|
NGINX_SERVER_NAME: ${NGINX_SERVER_NAME:-localhost}
|
|
NGINX_ENVSUBST_TEMPLATE_DIR: /etc/nginx/templates
|
|
NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx/conf.d
|
|
NGINX_ENVSUBST_TEMPLATE_SUFFIX: .template
|
|
healthcheck:
|
|
test: ["CMD", "wget", "-qO-", "--no-check-certificate", "http://localhost/healthz"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 10
|
|
start_period: 10s
|
|
|
|
certbot:
|
|
profiles: ["edge"]
|
|
image: certbot/certbot:latest
|
|
container_name: contract_check-certbot
|
|
restart: "no"
|
|
volumes:
|
|
- certbot-data:/etc/letsencrypt
|
|
- certbot-webroot:/var/www/certbot
|
|
entrypoint: /bin/sh
|
|
command:
|
|
- -c
|
|
- |
|
|
trap exit TERM
|
|
while :; do
|
|
certbot renew --webroot-path /var/www/certbot --quiet
|
|
sleep 12h & wait $${!}
|
|
done
|
|
|
|
nginx-exporter:
|
|
profiles: ["edge"]
|
|
image: nginx/nginx-prometheus-exporter:latest
|
|
container_name: contract_check-nginx-exporter
|
|
restart: unless-stopped
|
|
command:
|
|
- "-nginx.scrape-uri=http://nginx:80/stub_status"
|
|
depends_on:
|
|
nginx:
|
|
condition: service_healthy
|
|
ports:
|
|
- "${NGINX_EXPORTER_PORT:-9113}:9113"
|
|
|
|
# ── OBSERVABILITY (profile: obs) ───────────────────────────────────────────
|
|
# Grafana + Loki logs + Prometheus metrics. Promtail scrapes all compose
|
|
# container logs via the local Docker socket. See deploy/observability/.
|
|
prometheus:
|
|
profiles: ["obs"]
|
|
image: prom/prometheus:latest
|
|
container_name: contract_check-prometheus
|
|
restart: unless-stopped
|
|
command:
|
|
- "--config.file=/etc/prometheus/prometheus.yml"
|
|
- "--storage.tsdb.path=/prometheus"
|
|
- "--storage.tsdb.retention.time=15d"
|
|
- "--web.console.libraries=/usr/share/prometheus/console_libraries"
|
|
- "--web.console.templates=/usr/share/prometheus/consoles"
|
|
- "--web.enable-lifecycle"
|
|
volumes:
|
|
- prometheus-data:/prometheus
|
|
- ./deploy/observability/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
ports:
|
|
- "${PROMETHEUS_PORT:-9090}:9090"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:9090/-/healthy || exit 1"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 10
|
|
start_period: 15s
|
|
|
|
loki:
|
|
profiles: ["obs"]
|
|
image: grafana/loki
|
|
container_name: contract_check-loki
|
|
restart: unless-stopped
|
|
command: -config.file=/etc/loki/loki-config.yaml
|
|
volumes:
|
|
- loki-data:/loki
|
|
- ./deploy/observability/loki-config.yaml:/etc/loki/loki-config.yaml:ro
|
|
ports:
|
|
- "13100:3100"
|
|
|
|
promtail:
|
|
profiles: ["obs"]
|
|
image: grafana/promtail
|
|
container_name: contract_check-promtail
|
|
restart: unless-stopped
|
|
command: -config.file=/etc/promtail/promtail-config.yaml
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
- ./deploy/observability/promtail-config.yaml:/etc/promtail/promtail-config.yaml:ro
|
|
|
|
grafana:
|
|
profiles: ["obs"]
|
|
image: grafana/grafana
|
|
container_name: contract_check-grafana
|
|
restart: unless-stopped
|
|
volumes:
|
|
- grafana-data:/var/lib/grafana
|
|
- ./deploy/observability/grafana/provisioning:/etc/grafana/provisioning:ro
|
|
- ./deploy/observability/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
|
ports:
|
|
- "${GRAFANA_PORT:-3000}:3000"
|
|
environment:
|
|
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
|
|
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin}
|
|
GF_USERS_ALLOW_SIGN_UP: "false"
|
|
# When served under a subpath via nginx, Grafana must know the root URL.
|
|
GF_SERVER_ROOT_URL: "${GRAFANA_ROOT_URL:-http://localhost:3000}/grafana/"
|
|
GF_SERVER_SERVE_FROM_SUB_PATH: "true"
|
|
GF_INSTALL_PLUGINS: ""
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -sf http://localhost:3000/api/health || exit 1"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 10
|
|
start_period: 30s
|
|
|
|
volumes:
|
|
pgdata:
|
|
pgwal:
|
|
redisdata:
|
|
rabbitmq:
|
|
minio:
|
|
certbot-data:
|
|
certbot-webroot:
|
|
loki-data:
|
|
grafana-data:
|
|
prometheus-data:
|