# «Контракт-чек» — 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 environment: ENV: ${ENV:-dev} LOG_LEVEL: ${LOG_LEVEL:-INFO} 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 S3_ACCESS_KEY: ${S3_ACCESS_KEY:-contract_check} S3_SECRET_KEY: ${S3_SECRET_KEY:-contract_check} S3_BUCKET: ${S3_BUCKET:-contract-check-docs} S3_REGION: ${S3_REGION:-us-east-1} S3_SERVER_SIDE_ENCRYPTION: ${S3_SERVER_SIDE_ENCRYPTION:-false} DOC_RETENTION_DAYS: ${DOC_RETENTION_DAYS:-7} REFUND_POLICY: ${REFUND_POLICY:-all} SENTRY_DSN: ${SENTRY_DSN:-} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} OTEL_SERVICE_NAME: api LLM_PROVIDER: ${LLM_PROVIDER:-ollama_cloud} OLLAMA_HOST: ${OLLAMA_HOST:-} OLLAMA_API_KEY: ${OLLAMA_API_KEY:-} OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen2.5:14b} OLLAMA_FALLBACK_MODEL: ${OLLAMA_FALLBACK_MODEL:-qwen2.5:7b} OLLAMA_TEMPERATURE: ${OLLAMA_TEMPERATURE:-0.2} OLLAMA_NUM_PREDICT: ${OLLAMA_NUM_PREDICT:-3072} OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-120} OLLAMA_MAX_CONCURRENCY: ${OLLAMA_MAX_CONCURRENCY:-3} CHUNK_SIZE_CHARS: ${CHUNK_SIZE_CHARS:-10000} API_HOST: ${API_HOST:-0.0.0.0} API_PORT: ${API_PORT:-8000} API_METRICS_PORT: ${API_METRICS_PORT:-9100} TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-} JWT_SECRET: ${JWT_SECRET} JWT_ALGORITHM: ${JWT_ALGORITHM:-HS256} JWT_ACCESS_TTL_MINUTES: ${JWT_ACCESS_TTL_MINUTES:-1440} JWT_REFRESH_TTL_DAYS: ${JWT_REFRESH_TTL_DAYS:-30} WEB_AUTH_ENABLED: ${WEB_AUTH_ENABLED:-true} PASSWORD_RESET_TTL_MINUTES: ${PASSWORD_RESET_TTL_MINUTES:-60} PASSWORD_MIN_LENGTH: ${PASSWORD_MIN_LENGTH:-8} WEB_APP_BASE_URL: ${WEB_APP_BASE_URL:-http://localhost:5173} SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-587} SMTP_USERNAME: ${SMTP_USERNAME:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_FROM: ${SMTP_FROM:-no-reply@contract-check.local} SMTP_USE_TLS: ${SMTP_USE_TLS:-true} 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 environment: ENV: ${ENV:-dev} LOG_LEVEL: ${LOG_LEVEL:-INFO} 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 S3_ACCESS_KEY: ${S3_ACCESS_KEY:-contract_check} S3_SECRET_KEY: ${S3_SECRET_KEY:-contract_check} S3_BUCKET: ${S3_BUCKET:-contract-check-docs} S3_REGION: ${S3_REGION:-us-east-1} S3_SERVER_SIDE_ENCRYPTION: ${S3_SERVER_SIDE_ENCRYPTION:-false} DOC_RETENTION_DAYS: ${DOC_RETENTION_DAYS:-7} REFUND_POLICY: ${REFUND_POLICY:-all} SENTRY_DSN: ${SENTRY_DSN:-} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} OTEL_SERVICE_NAME: worker-extract MQ_PREFETCH_EXTRACT: ${MQ_PREFETCH_EXTRACT:-1} MQ_MAX_ATTEMPTS: ${MQ_MAX_ATTEMPTS:-5} MQ_RETRY_BASE_MS: ${MQ_RETRY_BASE_MS:-2000} TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-} JWT_SECRET: ${JWT_SECRET} JWT_ALGORITHM: ${JWT_ALGORITHM:-HS256} JWT_ACCESS_TTL_MINUTES: ${JWT_ACCESS_TTL_MINUTES:-1440} 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 environment: ENV: ${ENV:-dev} LOG_LEVEL: ${LOG_LEVEL:-INFO} 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 S3_ACCESS_KEY: ${S3_ACCESS_KEY:-contract_check} S3_SECRET_KEY: ${S3_SECRET_KEY:-contract_check} S3_BUCKET: ${S3_BUCKET:-contract-check-docs} S3_REGION: ${S3_REGION:-us-east-1} REFUND_POLICY: ${REFUND_POLICY:-all} SENTRY_DSN: ${SENTRY_DSN:-} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} OTEL_SERVICE_NAME: worker-analyze MQ_PREFETCH_ANALYZE: ${MQ_PREFETCH_ANALYZE:-3} MQ_MAX_ATTEMPTS: ${MQ_MAX_ATTEMPTS:-5} MQ_RETRY_BASE_MS: ${MQ_RETRY_BASE_MS:-2000} LLM_PROVIDER: ${LLM_PROVIDER:-ollama_cloud} OLLAMA_HOST: ${OLLAMA_HOST:-} OLLAMA_API_KEY: ${OLLAMA_API_KEY:-} OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen2.5:14b} OLLAMA_FALLBACK_MODEL: ${OLLAMA_FALLBACK_MODEL:-qwen2.5:7b} OLLAMA_TEMPERATURE: ${OLLAMA_TEMPERATURE:-0.2} OLLAMA_NUM_PREDICT: ${OLLAMA_NUM_PREDICT:-3072} OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-120} OLLAMA_MAX_CONCURRENCY: ${OLLAMA_MAX_CONCURRENCY:-3} CHUNK_SIZE_CHARS: ${CHUNK_SIZE_CHARS:-10000} TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-} JWT_SECRET: ${JWT_SECRET} JWT_ALGORITHM: ${JWT_ALGORITHM:-HS256} JWT_ACCESS_TTL_MINUTES: ${JWT_ACCESS_TTL_MINUTES:-1440} ports: - "9102:9102" 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 environment: ENV: ${ENV:-dev} LOG_LEVEL: ${LOG_LEVEL:-INFO} 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:-/} SENTRY_DSN: ${SENTRY_DSN:-} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} OTEL_SERVICE_NAME: worker-notify MQ_PREFETCH_NOTIFY: ${MQ_PREFETCH_NOTIFY:-5} MQ_MAX_ATTEMPTS: ${MQ_MAX_ATTEMPTS:-5} MQ_RETRY_BASE_MS: ${MQ_RETRY_BASE_MS:-2000} SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-587} SMTP_USERNAME: ${SMTP_USERNAME:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_FROM: ${SMTP_FROM:-no-reply@contract-check.local} SMTP_USE_TLS: ${SMTP_USE_TLS:-true} WEB_APP_BASE_URL: ${WEB_APP_BASE_URL:-http://localhost:5173} 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:-} volumes: pgdata: pgwal: redisdata: rabbitmq: minio: