# 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/.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/.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: