Deal — единая кодовая база
ci / build-test (push) Canceled after 0s

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 зелёные.
This commit is contained in:
Rustam Khalimov
2026-09-11 23:56:47 +03:00
commit 27c7831910
1383 changed files with 158436 additions and 0 deletions
+350
View File
@@ -0,0 +1,350 @@
# 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: