DealDocumentScreening/docker-compose.yml
febux 85db70887d Fix Vector config and OpenObserve auth for live E2E verification
Ticket 10 verification surfaced runtime issues that static validation
missed; all verified against running Docker stack (Vector 0.43 + OO 0.92.2):

- Rewrite VRL for Vector 0.43: no coalesce/default funcs, merge!/replace!
  for guarded fallible calls, no two-value parse_json destructuring
- Quote env-interpolated sink credentials (empty env left a bare YAML null)
- Fix logs sink URI: OO needs /api/{org}/{stream}/_json (stream segment
  was missing, causing 404); logs now land in contract_check stream
- Replace OPENOBSERVE_AUTH_TOKEN with root email/password Basic auth:
  prometheus_remote_write ignores request.headers, so remote-write got 401
- Update .env.example, DEPLOY.md, ARCHITECTURE.md, ticket 05 accordingly

Verified live: logs with service + correlation_id searchable in
OpenObserve; contract_check_* metrics queryable via its Prometheus API;
no outbound port 4318 connections from app containers.
2026-09-06 19:41:04 +03:00

528 lines
20 KiB
YAML

# «Контракт-чек» — infrastructure (Step 1).
#
# Default (`docker compose up`) starts ONLY infra: postgres + redis + rabbitmq
# + minio (+ minio-init). Service containers are added behind profiles:
# `services` -> api + workers (no bot)
# `bot` -> Telegram bot adapter (can run on a separate host)
# `obs` / `observer` / `edge` -> observability and reverse proxy
# (docs/ARCHITECTURE.md §20, docs/DEPLOY.md §14).
#
# 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
ports:
- "${API_PORT:-8000}:8000"
- "${API_METRICS_BIND_HOST:-127.0.0.1}:${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
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
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
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}
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
# Telegram bot adapter (aiogram 3, HTTP-only to api). Per docs/ARCHITECTURE.md §17
# the bot holds no DB/MQ/S3 credentials — it talks only to the API.
# It is intentionally isolated in profile `bot` so api + workers can start
# without it; the bot can also run on a different host. See docs/DEPLOY.md §14.
bot:
profiles: ["bot"]
build:
context: .
dockerfile: srv/bot/Dockerfile
container_name: contract_check-bot
restart: unless-stopped
# Note: no depends_on api. The bot healthchecks the API at runtime and
# restarts via `restart: unless-stopped` if the API is not yet ready.
# This keeps the bot profile self-contained and deployable on a separate host.
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:-}
# Update delivery: polling (default) or webhook. In webhook mode the bot
# serves updates + GET /healthz on port 8080 inside the container; the
# edge profile proxies the secret path (BOT_WEBHOOK_PATH) here.
# See docs/DEPLOY.md §14.4.
BOT_UPDATE_MODE: ${BOT_UPDATE_MODE:-polling}
BOT_WEBHOOK_PUBLIC_BASE_URL: ${BOT_WEBHOOK_PUBLIC_BASE_URL:-}
BOT_WEBHOOK_SECRET_TOKEN: ${BOT_WEBHOOK_SECRET_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
# Passed to the template so the OpenObserve subpath can be changed in one place.
OPENOBSERVE_BASE_URI: ${OPENOBSERVE_BASE_URI:-/openobserve}
# Secret-derived Telegram bot webhook path (print with `make
# bot-webhook-path`). Proxied to the bot service inside the TLS edge;
# the placeholder default keeps the config valid in polling mode.
BOT_WEBHOOK_PATH: ${BOT_WEBHOOK_PATH:-/tg-webhook/change-me}
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 ──────────────────────────────────────────────────────────
# Two observability stacks are provided; activate exactly one profile.
#
# profile: observer → OpenObserve via Vector (lightweight, single binary).
# Vector scrapes container logs from the Docker socket
# and scrapes service /metrics endpoints; application
# code does not push telemetry.
# profile: obs → Grafana + Prometheus + Loki (mature, heavier).
# Use for production-grade visibility.
#
# Vector (profile `observer`) authenticates to OpenObserve with
# OPENOBSERVE_ROOT_USER_EMAIL / OPENOBSERVE_ROOT_USER_PASSWORD. No OTLP
# endpoint configuration is needed in the app.
# ── LIGHTWEIGHT OBSERVABILITY (profile: observer) ───────────────────────────
openobserve:
profiles: ["observer"]
image: public.ecr.aws/zinclabs/openobserve:latest
container_name: contract_check-openobserve
restart: unless-stopped
environment:
ZO_DATA_DIR: /data
ZO_ROOT_USER_EMAIL: ${OPENOBSERVE_ROOT_USER_EMAIL:-root@example.com}
ZO_ROOT_USER_PASSWORD: ${OPENOBSERVE_ROOT_USER_PASSWORD:-Complexpass#123}
# Keep the lightweight single-node SQLite/local-disk backend by default.
# Switch to S3-backed single-node by setting ZO_LOCAL_MODE_STORAGE=s3 and
# configuring ZO_S3_* variables (see docs/administration/configuration/).
ZO_LOCAL_MODE_STORAGE: ${OPENOBSERVE_LOCAL_MODE_STORAGE:-disk}
# Must match the nginx location prefix (deploy/nginx/templates/).
ZO_BASE_URI: ${OPENOBSERVE_BASE_URI:-/openobserve}
volumes:
- openobserve-data:/data
ports:
- "${OPENOBSERVE_PORT:-5080}:5080" # UI + ingestion endpoints
deploy:
resources:
limits:
cpus: "${OPENOBSERVE_CPU_LIMIT:-2}"
memory: "${OPENOBSERVE_MEMORY_LIMIT:-2G}"
reservations:
cpus: "${OPENOBSERVE_CPU_RESERVATION:-0.5}"
memory: "${OPENOBSERVE_MEMORY_RESERVATION:-512M}"
vector:
profiles: ["observer"]
image: timberio/vector:0.43.0-alpine
container_name: contract_check-vector
restart: unless-stopped
environment:
OPENOBSERVE_ROOT_USER_EMAIL: ${OPENOBSERVE_ROOT_USER_EMAIL:-root@example.com}
OPENOBSERVE_ROOT_USER_PASSWORD: ${OPENOBSERVE_ROOT_USER_PASSWORD:-Complexpass#123}
METRICS_BEARER_TOKEN: ${METRICS_BEARER_TOKEN:-}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./deploy/observability/vector-config.yaml:/etc/vector/vector.yaml:ro
depends_on:
openobserve:
condition: service_started
# ── MATURE 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:
openobserve-data:
loki-data:
grafana-data:
prometheus-data: