Files
Deal/deploy/compose.prod.yml
T
Rustam Khalimov 27c7831910
ci / build-test (push) Canceled after 0s
Deal — единая кодовая база
SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
2026-09-11 23:56:47 +03:00

351 lines
20 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.
# compose.prod.yml — PROD-стек «Дейла» (этап 7, Ruling 6/7/9; план Task 14).
#
# ⚠ НЕ поднимать на dev-машине вместо compose.dev.yml: это production-конфигурация с
# fail-fast на секреты (см. ниже) и без dev-дефолтов.
#
# Состав (всё в одной внутренней сети compose, наружу — ТОЛЬКО caddy :80/:443):
# postgres, minio — хранилища БЕЗ host-портов (volume'ы);
# core (:5080 HTTP + :5082 gRPC-ингресс), telegram-service (:5101), ai-service (:5102),
# ml-service (:5103) — процессы «Дейла»; mTLS-транспорт — по env Ruling 6 (см. ниже);
# caddy — edge: TLS-терминация, статика фронта, reverse_proxy /api → core.
# loki/promtail/grafana/prometheus — observability (Ruling 7; метрики — этап 12, пакет A): ПРОФИЛЬ
# `observability` — поднимается только: docker compose --profile observability up -d
# (или ... up -d --profile observability). Prometheus scrape'ит /metrics
# (порт 9464) всех 4 процессов; Grafana — логи (Loki) и метрики (Prometheus).
#
# Секреты — ТОЛЬКО из env: шаблон deploy/.env.prod.example → скопируйте в deploy/.env.prod,
# заполните значения и запускайте с --env-file:
# cp deploy/.env.prod.example deploy/.env.prod # и заполнить (без дефолтных паролей!)
# docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml up -d --build
# docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml \
# --profile observability up -d --build # + Loki/Promtail/Grafana/Prometheus
# Проверка без движка: docker compose --env-file ... -f deploy/compose.prod.yml config
# Ключевые обязательные (без значения `config` упадёт с подсказкой): DEAL_PG_PASSWORD,
# DEAL_ENCRYPTION_KEY, DEAL_SERVICE_TOKEN, DEAL_TELEGRAM_SESSION_KEY, MINIO_ROOT_USER,
# MINIO_ROOT_PASSWORD, DEAL_OPERATOR_LOGIN/DEAL_OPERATOR_PASSWORD (пустые — оператор не создаётся,
# см. Ruling 1), DEAL_ALLOWED_ORIGINS.
#
# PROD-флаги кода (Ruling 5/9/10): RateLimit__Enabled=true (лимиты/защита входа), куки
# Cookies__Secure/OperatorCookies__Secure=true (TLS терминирует caddy), ForwardedHeaders__Enabled=true
# (доверенный прокси — caddy; KnownNetworks — приватные docker-сети хоста: core наружу не публикуется,
# единственный вход в него — caddy), Security__AllowedOrigins — origin фронта.
#
# mTLS внутреннего gRPC (Ruling 6, Task 13/14): DEAL_MTLS_ENABLED=0 — plaintext + service-token
# (как dev, но внутри изолированной сети); DEAL_MTLS_ENABLED=1 — Kestrel сервисов/ингресса требует
# клиентский сертификат. Сертификаты: deploy/certs (scripts/mtls-certs.sh) монтируются в
# /etc/deal/certs. ВАЖНО при включении mTLS:
# * схемы внутренних endpoint'ов поменять на https:// (DEAL_ML_ENDPOINT/DEAL_AI_ENDPOINT/
# DEAL_TELEGRAM_ENDPOINT/DEAL_CORE_INGRESS в .env.prod);
# * healthcheck'и ниже сами переключаются на TLS-пробу grpc_health_probe (клиентский сертификат из
# /etc/deal/certs; PEM-артефакты deal-client.crt/.key генерирует scripts/mtls-certs.sh);
# * DEAL_MTLS_CERT_PASSWORD — пароль PFX (серверных и клиентского).
#
# Логи (Ruling 7): процессы пишут Serilog JSON в stdout (docker-логи) + rolling-файл
# data/logs/deal-<процесс>.json в контейнере. У core /app/data — volume deal_api_data (файлы-логи
# переживают пересоздание); у сервисов канал наблюдаемости — docker-логи → promtail (файлы сервисов
# эфемерны). Уровень — env DEAL_LOG_LEVEL.
#
# Frontend: статика монтируется из ../src/frontend/dist (СОБРАТЬ ДО ПОДЪЁМА: cd src/frontend &&
# npm run build). Конфигурация caddy — deploy/caddy/Caddyfile (см. его шапку про домен/TLS).
# Общий hardening прикладных сервисов (BL-IMG-HARDEN): read-only root FS + tmpfs /tmp,
# запрет новых привилегий и сброс capabilities. Данные/логи — только в именованных volume или tmpfs.
x-service-hardening: &service-hardening
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: deal
POSTGRES_USER: deal
POSTGRES_PASSWORD: ${DEAL_PG_PASSWORD:?DEAL_PG_PASSWORD не задан (пароль Postgres deal)}
volumes:
- deal_pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U deal -d deal"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
# MinIO (файлы проектных карточек; бакет deal-files создаётся лениво при первом upload, Ruling 4).
# БЕЗ host-портов: наружу файлы ходят только через core→caddy. Healthcheck нет: minio-образ не несёт
# curl/mc; на core падение minio = фолбэк LocalFileStorage (данные — в volume, подъём лечит).
minio:
# Тег зафиксирован (как остальные образы); при обновлении — свежий RELEASE-тег из quay.io/minio/minio.
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER не задан}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD не задан}
volumes:
- deal_minio_data:/data
command: server /data
restart: unless-stopped
# core (deal-api): HTTP :5080 + gRPC-ингресс telegram-service :5082. Сквозной gRPC-режим
# (UseLocal=false), как compose.dev.yml; rate limiting/куки-Secure/прокси-заголовки — PROD (см. шапку).
core:
build:
context: ..
dockerfile: src/core/Deal.Api/Dockerfile
<<: *service-hardening
mem_limit: 1g
cpus: 1.5
environment:
ASPNETCORE_ENVIRONMENT: Production
ASPNETCORE_URLS: http://0.0.0.0:5080
GRPC_INGRESS_PORT: "5082"
ConnectionStrings__DealPostgres: "Host=postgres;Port=5432;Database=deal;Username=deal;Password=${DEAL_PG_PASSWORD:?DEAL_PG_PASSWORD не задан}"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан (service-token gRPC)}
DEAL_ENCRYPTION_KEY: ${DEAL_ENCRYPTION_KEY:?DEAL_ENCRYPTION_KEY не задан (AES-256-GCM, 32 байта base64)}
DEAL_OPERATOR_LOGIN: ${DEAL_OPERATOR_LOGIN:-}
DEAL_OPERATOR_PASSWORD: ${DEAL_OPERATOR_PASSWORD:-}
DEAL_DEFAULT_AI_BUDGET: ${DEAL_DEFAULT_AI_BUDGET:-}
# Безопасность PROD (Rulings 5/9/10): лимиты вкл., куки Secure, доверенный прокси (caddy).
RateLimit__Enabled: "true"
Cookies__Secure: "true"
OperatorCookies__Secure: "true"
ForwardedHeaders__Enabled: "true"
# Доверенные подсети прокси: приватные docker-сети хоста (caddy — единственный вход в core,
# host-портов у core нет). Свою подсеть можно уточнить: docker network inspect <сеть>.
ForwardedHeaders__KnownNetworks__0: 172.16.0.0/12
Security__AllowedOrigins: ${DEAL_ALLOWED_ORIGINS:?DEAL_ALLOWED_ORIGINS не задан (origin фронта, напр. https://deal.example)}
# Интеграции — gRPC-режим всего стека (как dev); схемы endpoint'ов — http:// (mTLS off) либо
# https:// при DEAL_MTLS_ENABLED=1 (см. .env.prod.example).
Services__Ml__UseLocal: "false"
Services__Ml__Endpoint: ${DEAL_ML_ENDPOINT:-http://ml-service:5103}
Services__Ai__UseLocal: "false"
Services__Ai__Endpoint: ${DEAL_AI_ENDPOINT:-http://ai-service:5102}
Services__Telegram__UseLocal: "false"
Services__Telegram__Endpoint: ${DEAL_TELEGRAM_ENDPOINT:-http://telegram-service:5101}
# Файлы — MinIO (внутренний http; TLS minio — вне этапа, при желании Storage__Minio__Secure=true
# + endpoint https и сертификаты).
Storage__Minio__Endpoint: minio:9000
Storage__Minio__AccessKey: ${MINIO_ROOT_USER:?MINIO_ROOT_USER не задан}
Storage__Minio__SecretKey: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD не задан}
Storage__Minio__Bucket: deal-files
Storage__Minio__Secure: "false"
# mTLS внутреннего gRPC (Ruling 6; пути — /etc/deal/certs, см. volume ниже).
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/core-server.pfx
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
volumes:
# data/: ключ шифрования (или DEAL_ENCRYPTION_KEY), LocalFileStorage-фолбэк, логи data/logs.
- deal_api_data:/app/data
# deploy/certs (scripts/mtls-certs.sh): нужны только при DEAL_MTLS_ENABLED=1.
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
depends_on:
postgres:
condition: service_healthy
healthcheck:
# gRPC-health ингресса :5082 (health освобождён от service-token). Под mTLS — TLS-проба
# с клиентским сертификатом deal-client и CA (PEM-артефакты генерирует scripts/mtls-certs.sh).
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5082 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5082; fi"]
interval: 10s
timeout: 3s
retries: 10
start_period: 15s
restart: unless-stopped
# telegram-service — gRPC-сервер Telegram (:5101; plaintext либо mTLS — как core). Сессии —
# /data/sessions (volume deal_tg_sessions, AES-GCM ключом DEAL_TELEGRAM_SESSION_KEY, fail-closed).
telegram-service:
build:
context: ..
dockerfile: src/telegram-service/Deal.Telegram/Dockerfile
<<: *service-hardening
mem_limit: 512m
cpus: 1.0
environment:
GRPC_PORT: "5101"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:?DEAL_TELEGRAM_SESSION_KEY не задан (ключ AES-GCM сессий)}
DEAL_TELEGRAM_SESSION_DIR: /data/sessions
DEAL_LOGS_DIR: /tmp/logs
SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082}
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/telegram-service-server.pfx
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
volumes:
- deal_tg_sessions:/data/sessions
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
healthcheck:
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5101 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5101; fi"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
# ai-service — gRPC-сервер ИИ-фасада (:5102). Без БД и volume'ов (конфиг провайдера — в теле RPC).
ai-service:
build:
context: ..
dockerfile: src/ai-service/Deal.Ai/Dockerfile
<<: *service-hardening
mem_limit: 512m
cpus: 1.0
environment:
GRPC_PORT: "5102"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
DEAL_LOGS_DIR: /tmp/logs
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ai-service-server.pfx
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
volumes:
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
healthcheck:
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5102 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5102; fi"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
# ml-service — gRPC-сервер ML (:5103). Модели — /data/ml (volume deal_ml_data, SQLite на тенанта).
ml-service:
build:
context: ..
dockerfile: src/ml-service/Deal.Ml/Dockerfile
<<: *service-hardening
mem_limit: 768m
cpus: 1.0
environment:
GRPC_PORT: "5103"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
DEAL_ML_DATA_DIR: /data/ml
DEAL_LOGS_DIR: /tmp/logs
DEAL_MTLS_ENABLED: ${DEAL_MTLS_ENABLED:-0}
DEAL_MTLS_CA_PEM: /etc/deal/certs/ca.pem
DEAL_MTLS_SERVER_CERT_PFX: /etc/deal/certs/ml-service-server.pfx
DEAL_MTLS_SERVER_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
DEAL_MTLS_CLIENT_CERT_PFX: /etc/deal/certs/deal-client.pfx
DEAL_MTLS_CLIENT_CERT_PASSWORD: ${DEAL_MTLS_CERT_PASSWORD:-}
volumes:
- deal_ml_data:/data/ml
- ${DEAL_CERTS_DIR:-./certs}:/etc/deal/certs:ro
healthcheck:
test: ["CMD-SHELL", "if [ \"$$DEAL_MTLS_ENABLED\" = \"1\" ]; then /bin/grpc_health_probe -addr=localhost:5103 -tls -tls-ca-cert=/etc/deal/certs/ca.pem -tls-client-cert=/etc/deal/certs/deal-client.crt -tls-client-key=/etc/deal/certs/deal-client.key -tls-server-name=localhost; else /bin/grpc_health_probe -addr=localhost:5103; fi"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
# caddy — edge: наружу только :80/:443. TLS — плейсхолдер tls internal (см. Caddyfile: домен,
# реальный сертификат/Cloudflare, CSP/HSTS). Статика — ../src/frontend/dist (СОБРАТЬ ДО up).
caddy:
image: caddy:2.9.1
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ../src/frontend/dist:/srv:ro
# /data — локальный CA и сертификаты Caddy (переживают пересоздание контейнера).
- deal_caddy_data:/data
- deal_caddy_config:/config
depends_on:
core:
condition: service_healthy
restart: unless-stopped
# ── Observability (Ruling 7/9; метрики — этап 12, пакет A) — ПРОФИЛЬ: --profile observability ──
# Логи: docker-логи (stdout контейнеров) → promtail → loki → grafana (127.0.0.1:3001).
# Метрики: /metrics процессов (:9464) → prometheus (127.0.0.1:9090) → grafana (датасорс uid
# `prometheus`). Доступ — оператору по SSH-туннелю; наружу grafana/prometheus НЕ публикуются.
# Версии образов — фиксированные (при подъёме обновите до актуальных patch-релизов).
loki:
image: grafana/loki:3.4.2
profiles: ["observability"]
command: -config.file=/etc/loki/loki.yml
volumes:
- ./observability/loki.yml:/etc/loki/loki.yml:ro
- deal_loki_data:/loki
restart: unless-stopped
# Prometheus — сбор метрик Deal-процессов (этап 12, пакет A): scrape /metrics каждого процесса
# (:9464, HTTP/1.1) внутри compose-сети. Конфиг — deploy/observability/prometheus.yml (таргеты
# core/telegram-service/ai-service/ml-service). UI — только loopback 127.0.0.1:9090 (оператору по
# SSH-туннелю, как Grafana); наружу порт не публикуется. Retention 15 суток (volume deal_prometheus_data).
prometheus:
image: prom/prometheus:v3.5.0
profiles: ["observability"]
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=15d
ports:
# Только loopback хоста — наружу не публикуется (Ruling 9).
- "127.0.0.1:9090:9090"
volumes:
- ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./observability/prometheus-rules.yml:/etc/prometheus/prometheus-rules.yml:ro
- deal_prometheus_data:/prometheus
restart: unless-stopped
promtail:
image: grafana/promtail:3.4.2
profiles: ["observability"]
command: -config.file=/etc/promtail/promtail.yml
volumes:
- ./observability/promtail.yml:/etc/promtail/promtail.yml:ro
# docker.sock — чтение docker-логов всех контейнеров (docker_sd). Rootless-докер: путь сокета
# свой (напр. /run/user/<uid>/docker.sock) — поправьте монтирование и promtail.yml.
- /var/run/docker.sock:/var/run/docker.sock:ro
- deal_promtail_data:/var/lib/promtail
depends_on:
loki:
condition: service_started
restart: unless-stopped
grafana:
image: grafana/grafana:11.5.2
profiles: ["observability"]
environment:
# Пароль задайте в .env.prod перед подъёмом профиля (пусто — grafana не получит admin-пароль).
GF_SECURITY_ADMIN_USER: ${DEAL_GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${DEAL_GRAFANA_ADMIN_PASSWORD:-}
GF_USERS_ALLOW_SIGN_UP: "false"
GF_AUTH_ANONYMOUS_ENABLED: "false"
ports:
# Только loopback хоста — оператору по SSH-туннелю (Ruling 9); наружу не публикуется.
- "127.0.0.1:3001:3000"
volumes:
- ./observability/grafana/provisioning:/etc/grafana/provisioning:ro
- ./observability/grafana/dashboards:/var/lib/grafana/dashboards:ro
- deal_grafana_data:/var/lib/grafana
depends_on:
loki:
condition: service_started
prometheus:
condition: service_started
restart: unless-stopped
volumes:
deal_pgdata:
deal_minio_data:
deal_tg_sessions:
deal_ml_data:
deal_api_data:
deal_caddy_data:
deal_caddy_config:
deal_loki_data:
deal_promtail_data:
deal_grafana_data:
deal_prometheus_data: