# ORCH-103 (Type B Bundled, ADR-001 D1–D4): самодостаточный compose ВСЕГО стека
# для тиража «под ключ» на хост заказчика: orchestrator + orchestrator-watchdog +
# Gitea + Plane CE (зеркало официального selfhost-référence makeplane/plane
# v0.23.1: имена сервисов и env-контракт — upstream, анти-дрейф к их докам; наши
# отличия от référence: пиннинг неподвижными тегами литералом вместо ${APP_RELEASE}
# (NFR-6, держится tests/test_bundle_compose.py), убраны replicas/platform/SENTRY,
# секреты БЕЗ дефолтных значений — их генерирует scripts/bootstrap_bundle.py).
#
# Этот файл НЕ исполняется в нашем прод-контуре (корневой docker-compose.yml —
# байт-в-байт, заморожен анти-дрейфом ORCH-102); активация — только явный запуск
# оператором на целевом хосте (паттерн ORCH-009, kill-switch не нужен).
#
# Конфиг-слои (D2): интерполяции ${VAR} читаются compose'ом из deploy/bundled/.env
# (авто-чтение из project dir — без --env-file-футгана); канон ключей —
# deploy/bundled/.env.example (key-set-sync держит тест). Runtime-конфиг орка и
# watchdog — КОРНЕВЫЕ .env / .env.watchdog (канон Lite 1:1, REPLICATION §2);
# их единственный писатель — bootstrap_bundle.py.
#
# Сеть (D4): одна bridge-сеть проекта; машинный трафик — строго сервис-DNS
# (Plane→орк http://orchestrator:8500/webhook/plane, Gitea→орк .../webhook/gitea,
# орк→Plane http://proxy, орк→Gitea http://gitea:3000); network_mode: host НЕ
# используется (ssh-деплой-контур нашего хоста в bundle структурно спит —
# ORCH_DEPLOY_SSH_HOST пуст). Наружу публикуются ТОЛЬКО человеческие порты
# (орк/Plane proxy/Gitea web); postgres/redis/mq/minio не публикуются.
#
# Project name = узнаваемый префикс томов/контейнеров orchestrator-bundle_* (D1);
# container_name сознательно НЕ пиннится ни у кого — bundle и Lite/корневой
# compose не сталкиваются по именам на одном хосте.
name: orchestrator-bundle

networks:
  default:
    name: orchestrator-bundle
    driver: bridge

# Env-контракт Plane CE — upstream-имена (référence v0.23.1). Значения секретов
# (POSTGRES_PASSWORD/SECRET_KEY/RABBITMQ_DEFAULT_PASS/MINIO_ROOT_PASSWORD) живут
# ТОЛЬКО в deploy/bundled/.env (генерирует bootstrap); дефолтных паролей нет.
x-plane-env: &plane-env
  environment:
    - WEB_URL=http://${BUNDLE_PUBLIC_HOST:-localhost}:${BUNDLE_PLANE_PORT:-8080}
    - DEBUG=0
    - CORS_ALLOWED_ORIGINS=http://${BUNDLE_PUBLIC_HOST:-localhost}:${BUNDLE_PLANE_PORT:-8080}
    - GUNICORN_WORKERS=1
    # db (upstream-имена; host/port — фиксированные сервис-DNS этого файла)
    - PGHOST=plane-db
    - PGDATABASE=${POSTGRES_DB:-plane}
    - POSTGRES_USER=${POSTGRES_USER:-plane}
    - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    - POSTGRES_DB=${POSTGRES_DB:-plane}
    - POSTGRES_PORT=5432
    - PGDATA=/var/lib/postgresql/data
    - DATABASE_URL=postgresql://${POSTGRES_USER:-plane}:${POSTGRES_PASSWORD}@plane-db:5432/${POSTGRES_DB:-plane}
    # redis
    - REDIS_HOST=plane-redis
    - REDIS_PORT=6379
    - REDIS_URL=redis://plane-redis:6379/
    # rabbitmq
    - RABBITMQ_HOST=plane-mq
    - RABBITMQ_PORT=5672
    - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER:-plane}
    - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS}
    - RABBITMQ_DEFAULT_VHOST=${RABBITMQ_DEFAULT_VHOST:-plane}
    - RABBITMQ_VHOST=${RABBITMQ_DEFAULT_VHOST:-plane}
    - AMQP_URL=amqp://${RABBITMQ_DEFAULT_USER:-plane}:${RABBITMQ_DEFAULT_PASS}@plane-mq:5672/${RABBITMQ_DEFAULT_VHOST:-plane}
    # application secret (генерирует bootstrap; дефолта сознательно НЕТ)
    - SECRET_KEY=${SECRET_KEY}
    # datastore (minio)
    - USE_MINIO=1
    - AWS_REGION=
    - AWS_ACCESS_KEY_ID=${MINIO_ROOT_USER:-plane-minio-admin}
    - AWS_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
    - AWS_S3_ENDPOINT_URL=http://plane-minio:9000
    - AWS_S3_BUCKET_NAME=uploads
    - MINIO_ROOT_USER=${MINIO_ROOT_USER:-plane-minio-admin}
    - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
    - BUCKET_NAME=uploads
    - FILE_SIZE_LIMIT=5242880
    # live server
    - API_BASE_URL=http://api:8000
    # proxy
    - NGINX_PORT=80

services:
  # ── Платформа: орк + sidecar-watchdog (образы собираются из этого же чекаута;
  #    корневой Dockerfile / watchdog/Dockerfile — без правок, NFR-1) ──────────
  orchestrator:
    build:
      context: ../..
      # ORCH-101 (D5): uid/gid/home двигаются ОДНОЙ группой с user: и таргетами
      # маунтов ниже (инвариант ORCH-040). Дефолты bundle нейтральны (D2).
      args:
        APP_UID: ${ORCH_RUN_UID:-1000}
        APP_GID: ${ORCH_RUN_GID:-1000}
        APP_HOME: ${ORCH_AGENT_HOME_DIR:-/home/orchestrator}
    restart: unless-stopped
    user: "${ORCH_RUN_UID:-1000}:${ORCH_RUN_GID:-1000}"
    init: true
    command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8500"]
    ports:
      # человеческая точка: операторский smoke `curl /health` (D4)
      - "${BUNDLE_ORCH_PORT:-8500}:8500"
    volumes:
      # данные/репозитории — bind ВНУТРИ project dir (uid-причины ORCH-040;
      # покрыты .gitignore: неякорный data/ + deploy/bundled/repos/)
      - ./data:/app/data
      - ./repos:/repos
      - /var/run/docker.sock:/var/run/docker.sock
      # LLM-предусловие хоста заказчика (bundle его НЕ поставляет, BRD §1.3)
      - ${ORCH_HOST_CLAUDE_CODE_DIR:-/usr/lib/node_modules/@anthropic-ai/claude-code}:/opt/claude-code:ro
      - ${ORCH_HOST_NODE_BIN:-/usr/bin/node}:/usr/bin/node:ro
      - ${ORCH_HOST_CLAUDE_DIR:-~/.claude}:${ORCH_AGENT_HOME_DIR:-/home/orchestrator}/.claude
      - ${ORCH_HOST_CLAUDE_JSON:-~/.claude.json}:${ORCH_AGENT_HOME_DIR:-/home/orchestrator}/.claude.json:ro
      # ssh-контур в bundle сознательно НЕ вводится (ADR D8): git-доступ агентов
      # — HTTP token-remote, деплой-хуки нашего хоста структурно спят.
    # runtime-конфиг орка собирает bootstrap (шаг 8); required:false — первый
    # `up -d` поднимает стек ДО сборки конфига (AC-1), орк жив без него.
    env_file:
      - path: ../../.env
        required: false
    environment:
      - ORCH_REPOS_DIR=/repos
    group_add:
      - "${ORCH_DOCKER_GID:-999}"

  orchestrator-watchdog:
    build:
      context: ../..
      dockerfile: watchdog/Dockerfile
    restart: unless-stopped
    init: true
    mem_limit: 128m
    mem_reservation: 32m
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./repos:/repos:ro
      - ./data:/app/data:ro
    env_file:
      - path: ../../.env.watchdog
        required: false
    environment:
      # bundle-сеть ≠ host-network Lite: метрики — по сервис-DNS; имя контейнера
      # орка детерминировано project name (container_name не пиннится, D1).
      # environment перекрывает env_file → когерентность механическая (TR-8).
      - WATCHDOG_METRICS_URL=http://orchestrator:8500/metrics
      - WATCHDOG_CONTAINERS=orchestrator-bundle-orchestrator-1
    group_add:
      - "${ORCH_DOCKER_GID:-999}"

  # ── Gitea (D6): официальный образ, НЕ rootless; init полностью автоматом —
  #    bootstrap создаёт админа/токен через `gitea admin ...` CLI в контейнере.
  #    Branch protection на main НЕ настраивается (норматив D10 ORCH-009/INV-4).
  gitea:
    image: gitea/gitea:1.22.6
    restart: unless-stopped
    ports:
      - "${BUNDLE_GITEA_HTTP_PORT:-3000}:3000"
    environment:
      - GITEA__database__DB_TYPE=sqlite3
      - GITEA__security__INSTALL_LOCK=true
      - GITEA__server__DOMAIN=${BUNDLE_PUBLIC_HOST:-localhost}
      - GITEA__server__ROOT_URL=http://${BUNDLE_PUBLIC_HOST:-localhost}:${BUNDLE_GITEA_HTTP_PORT:-3000}/
      # ssh-контур не вводится (D8): порт не публикуется, ssh выключен.
      - GITEA__server__DISABLE_SSH=true
      - GITEA__service__DISABLE_REGISTRATION=true
      # МИНА TR-4 (D4): Gitea по умолчанию режет webhook'и в приватные адреса —
      # без этой строки «задача не появилась» гарантирован.
      - GITEA__webhook__ALLOWED_HOST_LIST=orchestrator
    volumes:
      - gitea-data:/data
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:3000/api/healthz"]
      interval: 10s
      timeout: 5s
      retries: 12

  # ── Plane CE — зеркало upstream selfhost-référence v0.23.1 (D3) ────────────
  web:
    <<: *plane-env
    image: makeplane/plane-frontend:v0.23.1
    restart: unless-stopped
    command: node web/server.js web
    depends_on:
      - api
      - worker

  space:
    <<: *plane-env
    image: makeplane/plane-space:v0.23.1
    restart: unless-stopped
    command: node space/server.js space
    depends_on:
      - api
      - worker
      - web

  admin:
    <<: *plane-env
    image: makeplane/plane-admin:v0.23.1
    restart: unless-stopped
    command: node admin/server.js admin
    depends_on:
      - api
      - web

  live:
    <<: *plane-env
    image: makeplane/plane-live:v0.23.1
    restart: unless-stopped
    command: node live/dist/server.js live
    depends_on:
      - api
      - web

  api:
    <<: *plane-env
    image: makeplane/plane-backend:v0.23.1
    restart: unless-stopped
    command: ./bin/docker-entrypoint-api.sh
    volumes:
      - logs_api:/code/plane/logs
    depends_on:
      - plane-db
      - plane-redis
      - plane-mq

  worker:
    <<: *plane-env
    image: makeplane/plane-backend:v0.23.1
    restart: unless-stopped
    command: ./bin/docker-entrypoint-worker.sh
    volumes:
      - logs_worker:/code/plane/logs
    depends_on:
      - api
      - plane-db
      - plane-redis
      - plane-mq

  beat-worker:
    <<: *plane-env
    image: makeplane/plane-backend:v0.23.1
    restart: unless-stopped
    command: ./bin/docker-entrypoint-beat.sh
    volumes:
      - logs_beat-worker:/code/plane/logs
    depends_on:
      - api
      - plane-db
      - plane-redis
      - plane-mq

  migrator:
    <<: *plane-env
    image: makeplane/plane-backend:v0.23.1
    restart: "no"
    command: ./bin/docker-entrypoint-migrator.sh
    volumes:
      - logs_migrator:/code/plane/logs
    depends_on:
      - plane-db
      - plane-redis

  plane-db:
    <<: *plane-env
    image: postgres:15.7-alpine
    restart: unless-stopped
    command: postgres -c 'max_connections=1000'
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 12

  plane-redis:
    <<: *plane-env
    image: valkey/valkey:7.2.5-alpine
    restart: unless-stopped
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "valkey-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 12

  plane-mq:
    <<: *plane-env
    image: rabbitmq:3.13.6-management-alpine
    restart: always
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
      interval: 15s
      timeout: 10s
      retries: 12

  plane-minio:
    <<: *plane-env
    # upstream-référence держит latest — bundle пиннит неподвижный тег (NFR-6)
    image: minio/minio:RELEASE.2024-05-28T17-19-04Z
    restart: unless-stopped
    command: server /export --console-address ":9090"
    volumes:
      - uploads:/export
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:9000/minio/health/live"]
      interval: 10s
      timeout: 5s
      retries: 12

  proxy:
    <<: *plane-env
    image: makeplane/plane-proxy:v0.23.1
    restart: unless-stopped
    ports:
      # человеческая точка: UI Plane в браузере оператора (D4)
      - "${BUNDLE_PLANE_PORT:-8080}:80"
    depends_on:
      - web
      - api
      - space

# Состояние Plane/Gitea — именованные тома проекта (префикс orchestrator-bundle_,
# D1/D2); preflight bootstrap детектирует «грязный хост» по этому префиксу.
volumes:
  pgdata:
  redisdata:
  uploads:
  logs_api:
  logs_worker:
  logs_beat-worker:
  logs_migrator:
  rabbitmq_data:
  gitea-data:
