Files
Deal/deploy/compose.prod.yml
T
Rustam Khalimov 9e07568ddd Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
2026-09-11 02:50:17 +03:00

325 lines
19 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).
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
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
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
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
environment:
GRPC_PORT: "5102"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
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
environment:
GRPC_PORT: "5103"
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:?DEAL_SERVICE_TOKEN не задан}
DEAL_ML_DATA_DIR: /data/ml
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: