Сервис frontend: сборка SPA (Vite) и отдача через Caddy, /api → core:5080. В профиль observability добавлены Loki/Promtail/Grafana (в dev их не было). Dev-Caddyfile отдаёт SPA с Cache-Control no-cache, чтобы пересборка подхватывалась обновлением страницы.
408 lines
19 KiB
YAML
408 lines
19 KiB
YAML
# compose.dev.yml — dev-стек Дейла (этап 6, Task 20; Ruling 12).
|
||
#
|
||
# Полный стек: postgres + minio (хранилища), три автономных сервиса этапа 6
|
||
# (telegram-service :5101, ai-service :5102, ml-service :5103, storage-service :5104) и core (deal-api:
|
||
# HTTP :5080 + gRPC-ингресс :5082).
|
||
#
|
||
# Режимы интеграций core — флагами SERVICES__{ML,AI,TELEGRAM}__USELOCAL (Ruling 6):
|
||
# * этот файл — сквозной gRPC-режим ВСЕГО стека «по-настоящему»: UseLocal=false заданы env
|
||
# core-сервиса ниже, интеграции ходят в ml/ai/telegram-сервисы этой же compose-сети
|
||
# (MlOutboxFlushScheduler → TrainBatch, GrpcAiClassifier/GrpcAiTools, GrpcTelegramClient).
|
||
# Подъём всего стека одной командой: docker compose -f deploy/compose.dev.yml up -d --build
|
||
# (см. также scripts/dev-smoke.sh — сквозная проверка с авто-очисткой).
|
||
# * Local-заглушки (дефолт КОДА — appsettings.json, UseLocal:true) сохраняются для запуска core
|
||
# с хоста БЕЗ сервисов (обычная dev-разработка, curl-приёмки этапов 2–5): поднимаются только
|
||
# postgres/minio, core — dotnet run из src/core/Deal.Api. Код-дефолт не меняется — режим
|
||
# переключает этот compose-файл (env ниже).
|
||
#
|
||
# Секреты dev (переопределяются .env/экспортом, шаблон ${VAR:-default}):
|
||
# DEAL_SERVICE_TOKEN — единый service-token всех процессов (metadata gRPC, Ruling 1/2/13)
|
||
# DEAL_ENCRYPTION_KEY — ключ AES-256-GCM шифрования секретов настроек core (32 байта base64)
|
||
# DEAL_TELEGRAM_SESSION_KEY — ключ AES-256-GCM файлов сессий telegram-service (32 байта base64)
|
||
# минио/БД creds — dev-значения ниже (в проде — этап 7, compose-prod + mTLS).
|
||
#
|
||
# Core в этом файле — «весь стек в docker» (эндпоинты — имена compose-сети). Альтернатива — core
|
||
# с хоста (Ruling 12): поднимаются только postgres/minio и (по желанию) сервисы, core запускается
|
||
# dotnet run, и сервисы ходят в ингресс хоста через DEAL_CORE_INGRESS=http://host.docker.internal:5082
|
||
# (см. env telegram-service ниже).
|
||
|
||
# Общий hardening прикладных сервисов (BL-IMG-HARDEN): read-only root FS + tmpfs /tmp,
|
||
# запрет новых привилегий и сброс capabilities. Данные — только в именованных volume.
|
||
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
|
||
container_name: deal-postgres
|
||
environment:
|
||
POSTGRES_DB: deal
|
||
POSTGRES_USER: deal
|
||
POSTGRES_PASSWORD: deal_dev_password
|
||
ports:
|
||
- "5433:5432" # 5432 может быть занят LeadRadar-стеком
|
||
volumes:
|
||
- deal_pgdata:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U deal -d deal"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 10
|
||
|
||
# MinIO для файлов проектных карточек (Ruling 4, план Task 6; образца-прототипа object_store.py).
|
||
# Опционально: API работает и без него — LocalFileStorage (data/attachments) выбирается, пока MinIO не
|
||
# сконфигурирован (Storage__Minio__* / DEAL_MINIO_*). Бакет deal-files создаётся приложением лениво при
|
||
# первом upload (Ruling 4: EnsureBucket на первом put), отдельный init-контейнер не нужен.
|
||
# Консоль: http://localhost:9001 (root deal_minio / deal_minio_secret).
|
||
# Если 9000/9001 заняты LeadRadar-minio — поменяйте на 9100/9101 (и endpoint в конфигурации API).
|
||
minio:
|
||
image: minio/minio
|
||
container_name: deal-minio
|
||
environment:
|
||
MINIO_ROOT_USER: deal_minio
|
||
MINIO_ROOT_PASSWORD: deal_minio_secret
|
||
ports:
|
||
- "9000:9000" # S3 API
|
||
- "9001:9001" # Web-консоль
|
||
volumes:
|
||
- deal_minio_data:/data
|
||
command: server /data --console-address ":9001"
|
||
|
||
# core (deal-api) — HTTP :5080 + gRPC-ингресс telegram-service :5082 (план Task 12/20, Ruling 7/12).
|
||
# Здесь core поднимается в сквозном gRPC-режиме (UseLocal=false ниже): сервисы этой же сети
|
||
# вызываются по-настоящему; MlOutbox копится в БД и выгружается MlOutboxFlushScheduler (10 с).
|
||
# Local-режим (дефолт appsettings.json) остаётся для запуска core с хоста без сервисов.
|
||
core:
|
||
build:
|
||
context: ..
|
||
dockerfile: src/core/Deal.Api/Dockerfile
|
||
container_name: deal-core
|
||
<<: *service-hardening
|
||
mem_limit: 1g
|
||
cpus: 1.5
|
||
environment:
|
||
ASPNETCORE_ENVIRONMENT: Development
|
||
ASPNETCORE_URLS: http://0.0.0.0:5080
|
||
GRPC_INGRESS_PORT: "5082"
|
||
ConnectionStrings__DealPostgres: Host=postgres;Port=5432;Database=deal;Username=deal;Password=deal_dev_password
|
||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||
DEAL_ENCRYPTION_KEY: ${DEAL_ENCRYPTION_KEY:-MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=}
|
||
# gRPC-режим всего стека (Ruling 6): Local-заглушки кода (appsettings default true)
|
||
# переключаются на gRPC-клиентов сервисов compose-сети. Хост-запуск core без compose
|
||
# остаётся на Local (код-дефолт не менялся).
|
||
Services__Ml__UseLocal: "false"
|
||
Services__Ai__UseLocal: "false"
|
||
Services__Telegram__UseLocal: "false"
|
||
Services__Ml__Endpoint: http://ml-service:5103
|
||
Services__Ai__Endpoint: http://ai-service:5102
|
||
Services__Telegram__Endpoint: http://telegram-service:5101
|
||
Services__Storage__Endpoint: http://storage-service:5104
|
||
Storage__Minio__Endpoint: minio:9000
|
||
Storage__Minio__AccessKey: deal_minio
|
||
Storage__Minio__SecretKey: deal_minio_secret
|
||
Storage__Minio__Bucket: deal-files
|
||
Storage__Minio__Secure: "false"
|
||
# Трейсинг OTel → коллектор профиля observability (без коллектора трейсы не экспортируются).
|
||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||
OTEL_SERVICE_NAME: core
|
||
ports:
|
||
- "5080:5080"
|
||
- "5082:5082" # gRPC-ингресс telegram-service (сервисы ходят на http://core:5082 внутри сети)
|
||
volumes:
|
||
- deal_api_data:/app/data # data/encryption.key + data/attachments (Local-фолбэк файлов)
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5082"]
|
||
interval: 10s
|
||
timeout: 3s
|
||
retries: 10
|
||
start_period: 15s
|
||
|
||
# telegram-service — gRPC-сервер Telegram (этап 6, Ruling 12; план Task 2). Отдельный процесс,
|
||
# отдельное sln (src/telegram-service/Deal.Telegram.sln). Команды ядра приходят на localhost:5101
|
||
# (SERVICES__Telegram__Endpoint — задачи 9–12), исходящий поток — в core-ингресс :5082
|
||
# (SERVICES__CORE__INGRESS, Ruling 7; внутри compose — http://core:5082; при core на хосте задайте
|
||
# DEAL_CORE_INGRESS=http://host.docker.internal:5082). Plaintext gRPC без mTLS (Ruling 2 — dev):
|
||
# защита — общий service-token (DEAL_SERVICE_TOKEN, env хоста и контейнеров). Сессии тенантов —
|
||
# файлы /data/sessions (Ruling 3), шифруются AES-256-GCM ключом DEAL_TELEGRAM_SESSION_KEY
|
||
# (fail-closed: без ключа сервис не стартует). Контекст сборки — корень репозитория: csproj
|
||
# ссылается на src/contracts/Deal.Proto.csproj (общий проект кодогенерации, Task 1).
|
||
telegram-service:
|
||
build:
|
||
context: ..
|
||
dockerfile: src/telegram-service/Deal.Telegram/Dockerfile
|
||
container_name: deal-telegram-service
|
||
<<: *service-hardening
|
||
mem_limit: 512m
|
||
cpus: 1.0
|
||
environment:
|
||
GRPC_PORT: "5101"
|
||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||
DEAL_TELEGRAM_SESSION_KEY: ${DEAL_TELEGRAM_SESSION_KEY:-ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMTA=}
|
||
DEAL_TELEGRAM_SESSION_DIR: /data/sessions
|
||
DEAL_LOGS_DIR: /tmp/logs
|
||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||
OTEL_SERVICE_NAME: telegram-service
|
||
SERVICES__CORE__INGRESS: ${DEAL_CORE_INGRESS:-http://core:5082}
|
||
ports:
|
||
- "5101:5101"
|
||
volumes:
|
||
- deal_tg_sessions:/data/sessions
|
||
healthcheck:
|
||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5101"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 10
|
||
|
||
# ai-service — gRPC-сервер ИИ-фасада (этап 6, Ruling 12; план Task 4). Отдельный процесс,
|
||
# отдельное sln (src/ai-service/Deal.Ai.sln). Промпты/классификацию/фильтр/ключи ядро шлёт на
|
||
# localhost:5102 (SERVICES__Ai__Endpoint — задачи 7/8/15). Plaintext gRPC без mTLS (Ruling 2 —
|
||
# dev): защита — общий service-token (DEAL_SERVICE_TOKEN, env хоста и контейнеров). Сервис без БД
|
||
# (Ruling 5): конфиг провайдера (id/base/model/apiKey/api_style) приходит в теле каждого запроса
|
||
# от ядра, персистентных файлов нет — volume не нужен. Контекст сборки — корень репозитория:
|
||
# csproj ссылается на src/contracts/Deal.Proto.csproj (общий проект кодогенерации, Task 1).
|
||
ai-service:
|
||
build:
|
||
context: ..
|
||
dockerfile: src/ai-service/Deal.Ai/Dockerfile
|
||
container_name: deal-ai-service
|
||
<<: *service-hardening
|
||
mem_limit: 512m
|
||
cpus: 1.0
|
||
environment:
|
||
GRPC_PORT: "5102"
|
||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||
DEAL_LOGS_DIR: /tmp/logs
|
||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||
OTEL_SERVICE_NAME: ai-service
|
||
ports:
|
||
- "5102:5102"
|
||
healthcheck:
|
||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5102"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 10
|
||
|
||
# ml-service — gRPC-сервер ML (этап 6, Ruling 12; план Task 3). Отдельный процесс, отдельное sln
|
||
# (src/ml-service/Deal.Ml.sln). Обучение и предсказания ядро шлёт на localhost:5103
|
||
# (SERVICES__Ml__Endpoint — задачи 5/6/16). Plaintext gRPC без mTLS (Ruling 2 — dev): защита —
|
||
# общий service-token (DEAL_SERVICE_TOKEN, env хоста и контейнеров). Модели тенантов — файлы
|
||
# SQLite /data/ml/<tenantId>.sqlite (Ruling 4). Контекст сборки — корень репозитория: csproj
|
||
# ссылается на src/contracts/Deal.Proto.csproj (общий проект кодогенерации, Task 1).
|
||
ml-service:
|
||
build:
|
||
context: ..
|
||
dockerfile: src/ml-service/Deal.Ml/Dockerfile
|
||
container_name: deal-ml-service
|
||
<<: *service-hardening
|
||
mem_limit: 768m
|
||
cpus: 1.0
|
||
environment:
|
||
GRPC_PORT: "5103"
|
||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||
DEAL_ML_DATA_DIR: /data/ml # файлы моделей data/ml/<tenantId>.sqlite на volume deal_ml_data (Ruling 4/12)
|
||
DEAL_LOGS_DIR: /tmp/logs
|
||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||
OTEL_SERVICE_NAME: ml-service
|
||
ports:
|
||
- "5103:5103"
|
||
volumes:
|
||
- deal_ml_data:/data/ml
|
||
healthcheck:
|
||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5103"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 10
|
||
|
||
# storage-service — общий gRPC-сервис данных (вложения источников). Отдельный процесс, отдельное sln
|
||
# (src/storage-service/Deal.Storage.sln). Загрузка/выгрузка/стат/удаление объектов; тип объекта
|
||
# определяет сервис. Бэкенд — MinIO. Защита — общий service-token (DEAL_SERVICE_TOKEN).
|
||
storage-service:
|
||
build:
|
||
context: ..
|
||
dockerfile: src/storage-service/Deal.Storage/Dockerfile
|
||
container_name: deal-storage-service
|
||
<<: *service-hardening
|
||
mem_limit: 512m
|
||
cpus: 1.0
|
||
environment:
|
||
GRPC_PORT: "5104"
|
||
DEAL_SERVICE_TOKEN: ${DEAL_SERVICE_TOKEN:-deal_dev_service_token}
|
||
DEAL_STORAGE_ENDPOINT: minio:9000
|
||
DEAL_STORAGE_ACCESS_KEY: deal_minio
|
||
DEAL_STORAGE_SECRET_KEY: deal_minio_secret
|
||
DEAL_STORAGE_BUCKET: deal-attachments
|
||
DEAL_STORAGE_SECURE: "false"
|
||
DEAL_LOGS_DIR: /tmp/logs
|
||
OTEL_EXPORTER_OTLP_ENDPOINT: ${DEAL_OTEL_ENDPOINT:-}
|
||
OTEL_SERVICE_NAME: storage-service
|
||
ports:
|
||
- "5104:5104"
|
||
depends_on:
|
||
- minio
|
||
healthcheck:
|
||
test: ["CMD", "/bin/grpc_health_probe", "-addr=localhost:5104"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 10
|
||
|
||
# Фронтенд (SPA) — сборка образа (Vite) и отдача через Caddy; /api → core:5080. UI — http://localhost:8080.
|
||
frontend:
|
||
build:
|
||
context: ..
|
||
dockerfile: src/frontend/Dockerfile
|
||
container_name: deal-frontend
|
||
ports:
|
||
- "8080:80"
|
||
depends_on:
|
||
core:
|
||
condition: service_healthy
|
||
|
||
# Prometheus (профиль observability, этап 12/пакет A) — сбор /metrics всех 4 процессов (:9464)
|
||
# внутри dev-сети. Подъём: docker compose -f deploy/compose.dev.yml --profile observability up -d.
|
||
# Конфиг — общий deploy/observability/prometheus.yml (те же имена сервисов и таргеты). UI — 9090.
|
||
prometheus:
|
||
image: prom/prometheus:v3.5.0
|
||
container_name: deal-prometheus
|
||
profiles: ["observability"]
|
||
command:
|
||
- --config.file=/etc/prometheus/prometheus.yml
|
||
- --storage.tsdb.path=/prometheus
|
||
- --storage.tsdb.retention.time=15d
|
||
ports:
|
||
- "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
|
||
|
||
# OpenTelemetry Collector — приём трейсов Deal-процессов (OTLP) → Tempo (профиль observability).
|
||
otel-collector:
|
||
image: otel/opentelemetry-collector-contrib:0.160.0
|
||
container_name: deal-otel-collector
|
||
profiles: ["observability"]
|
||
command: ["--config=/etc/otelcol-contrib/config.yaml"]
|
||
ports:
|
||
- "4317:4317"
|
||
- "4318:4318"
|
||
volumes:
|
||
- ./observability/otel-collector.yml:/etc/otelcol-contrib/config.yaml:ro
|
||
depends_on:
|
||
tempo:
|
||
condition: service_started
|
||
|
||
# Tempo — хранилище трейсов (OTLP от коллектора), UI/API — :3200 (профиль observability).
|
||
tempo:
|
||
image: grafana/tempo:2.8.1
|
||
container_name: deal-tempo
|
||
profiles: ["observability"]
|
||
command: ["-config.file=/etc/tempo.yml"]
|
||
ports:
|
||
- "3200:3200"
|
||
volumes:
|
||
- ./observability/tempo.yml:/etc/tempo.yml:ro
|
||
- deal_tempo_data:/var/tempo
|
||
|
||
# cAdvisor — ресурсы контейнеров (CPU/RAM/сеть/диск); scrape — job cadvisor.
|
||
cadvisor:
|
||
image: gcr.io/cadvisor/cadvisor:v0.52.1
|
||
container_name: deal-cadvisor
|
||
profiles: ["observability"]
|
||
privileged: true
|
||
devices:
|
||
- /dev/kmsg:/dev/kmsg
|
||
volumes:
|
||
- /:/rootfs:ro
|
||
- /var/run:/var/run:ro
|
||
- /sys:/sys:ro
|
||
- /var/lib/docker/:/var/lib/docker:ro
|
||
- /dev/disk/:/dev/disk:ro
|
||
|
||
# node-exporter — ресурсы хоста (CPU/RAM/диск/сеть); scrape — job node-exporter.
|
||
node-exporter:
|
||
image: prom/node-exporter:v1.9.1
|
||
container_name: deal-node-exporter
|
||
profiles: ["observability"]
|
||
command:
|
||
- --path.procfs=/host/proc
|
||
- --path.sysfs=/host/sys
|
||
- --path.rootfs=/host/root
|
||
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($|/)
|
||
ports:
|
||
- "9100:9100"
|
||
volumes:
|
||
- /proc:/host/proc:ro
|
||
- /sys:/host/sys:ro
|
||
- /:/host/root:ro
|
||
pid: host
|
||
|
||
# Loki — хранилище логов (профиль observability), UI/API — :3100.
|
||
loki:
|
||
image: grafana/loki:3.4.2
|
||
container_name: deal-loki
|
||
profiles: ["observability"]
|
||
command: -config.file=/etc/loki/loki.yml
|
||
ports:
|
||
- "3100:3100"
|
||
volumes:
|
||
- ./observability/loki.yml:/etc/loki/loki.yml:ro
|
||
- deal_loki_data:/loki
|
||
|
||
# Promtail — сбор docker-логов deal-процессов в Loki (docker.sock, профиль observability).
|
||
promtail:
|
||
image: grafana/promtail:3.4.2
|
||
container_name: deal-promtail
|
||
profiles: ["observability"]
|
||
command: -config.file=/etc/promtail/promtail.yml
|
||
volumes:
|
||
- ./observability/promtail.yml:/etc/promtail/promtail.yml:ro
|
||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||
- deal_promtail_data:/var/lib/promtail
|
||
depends_on:
|
||
loki:
|
||
condition: service_started
|
||
|
||
# Grafana — UI логов/метрик/трейсов (профиль observability), локальный вход admin/admin.
|
||
grafana:
|
||
image: grafana/grafana:11.5.2
|
||
container_name: deal-grafana
|
||
profiles: ["observability"]
|
||
environment:
|
||
GF_SECURITY_ADMIN_USER: admin
|
||
GF_SECURITY_ADMIN_PASSWORD: admin
|
||
GF_USERS_ALLOW_SIGN_UP: "false"
|
||
GF_AUTH_ANONYMOUS_ENABLED: "false"
|
||
ports:
|
||
- "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
|
||
tempo:
|
||
condition: service_started
|
||
|
||
volumes:
|
||
deal_pgdata:
|
||
deal_minio_data:
|
||
deal_tg_sessions:
|
||
deal_ml_data:
|
||
deal_api_data:
|
||
deal_prometheus_data:
|
||
deal_tempo_data:
|
||
deal_loki_data:
|
||
deal_promtail_data:
|
||
deal_grafana_data:
|