# ORCH-101 (replication foundation): every host-specific value is interpolated # as ${VAR:-default}; the defaults equal the current production values, so an # empty environment resolves to a byte-for-byte equivalent of the previous file # (zero regression, BR-5). Compose reads ${VAR} from the project `.env` /shell — # NOT from a service's env_file (so .env.staging does NOT interpolate); the # Settings-shared names (ORCH_AGENT_HOME_DIR, ORCH_STAGING_PORT, ...) are read # by pydantic from env_file AND by compose from .env — one name per fact (D1). # Container-side paths (/app/data, /repos, /opt/claude-code, docker.sock) are a # container-layout convention, NOT host values — deliberately not parametrised. # See docs/operations/REPLICATION.md for the full variable map. services: orchestrator: build: context: . # ORCH-101 (D5): uid/gid/home move as ONE coherent group with the runtime # user: and the mount targets below (ORCH-040 invariant). args: APP_UID: ${ORCH_RUN_UID:-1000} APP_GID: ${ORCH_RUN_GID:-1000} APP_HOME: ${ORCH_AGENT_HOME_DIR:-/home/slin} container_name: orchestrator restart: unless-stopped # ORCH-040: бежим под uid:gid хоста (slin=1000:1000), а не root, чтобы # артефакты конвейера (worktree + docs) создавались как slin:slin и git на # хосте работал без ручного chown. Доступ к docker.sock сохранён через # group_add: ["999"] (МИНА 1 — НЕ удалять). См. ADR-001 ORCH-040. user: "${ORCH_RUN_UID:-1000}:${ORCH_RUN_GID:-1000}" # init: true injects docker-init (tini) as PID 1 so reparented grandchild # processes from the claude/node subprocess tree are reaped (no zombies, B-2). init: true network_mode: host # ORCH-101 (D5): the prod port is configurable on the compose layer (the # Dockerfile CMD keeps its exec-form 8500 default — ADR-001 D5); the default # resolves byte-for-byte to the previous image CMD. Reuses the existing # ORCH_DEPLOY_PROD_TARGET_PORT (no second truth about the prod port). command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "${ORCH_DEPLOY_PROD_TARGET_PORT:-8500}"] volumes: - ./data:/app/data - ${ORCH_HOST_REPOS_DIR:-/home/slin/repos}:/repos - /var/run/docker.sock:/var/run/docker.sock - ${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:-/home/slin/.claude}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.claude - ${ORCH_HOST_CLAUDE_JSON:-/home/slin/.claude.json}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.claude.json:ro # ORCH-040: target согласован с HOME (launcher: settings.agent_home_dir), # не /root/.ssh — обе стороны двигаются одной переменной ORCH_AGENT_HOME_DIR. - ${ORCH_HOST_SSH_DIR:-/home/slin/.orchestrator-ssh}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.ssh:ro env_file: .env environment: - ORCH_REPOS_DIR=/repos - ORCH_HOST_REPOS_DIR=${ORCH_HOST_REPOS_DIR:-/home/slin/repos} # legacy enduro deployer (read via os.environ, keep as-is): - DEPLOY_SSH_USER=${ORCH_DEPLOY_SSH_USER:-slin} - DEPLOY_SSH_HOST=127.0.0.1 - DEPLOY_HOOK_SCRIPT=${DEPLOY_HOOK_SCRIPT:-/home/slin/bin/enduro-deploy-hook.sh} # ORCH-036 self-deploy (read via pydantic ORCH_ prefix; host-network -> 127.0.0.1, ssh key mounted): - ORCH_DEPLOY_SSH_USER=${ORCH_DEPLOY_SSH_USER:-slin} - ORCH_DEPLOY_SSH_HOST=127.0.0.1 - ORCH_DEPLOY_HOOK_SCRIPT=scripts/orchestrator-deploy-hook.sh - ORCH_DEPLOY_HOST_REPO_PATH=${ORCH_DEPLOY_HOST_REPO_PATH:-/home/slin/repos/orchestrator} group_add: - "${ORCH_DOCKER_GID:-999}" # ORCH-100 (FND/F1b): sidecar-watchdog — the monitoring brain in a SEPARATE # container (observer separated from observed, ADR-001 D2). Deploying it builds # ONLY this service — the prod `orchestrator` is NOT rebuilt/restarted. # * network_mode: host -> /metrics reachable at http://127.0.0.1:8500/metrics # and host interfaces visible for memory/disk reads. # * docker.sock mounted :ro AND the code is GET-only (double read-only guard). # * host disk paths bind-mounted :ro so shutil.disk_usage sees the host FS but # can never write (opt-in disk ceiling, D6). # * mem_limit caps the thin stdlib daemon (D2): OOM = early "sidecar grew" signal. # * WATCHDOG_ENABLED=false (or simply not starting the service) -> inert. orchestrator-watchdog: build: context: . dockerfile: watchdog/Dockerfile container_name: orchestrator-watchdog restart: unless-stopped init: true network_mode: host mem_limit: 128m mem_reservation: 32m volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ${ORCH_HOST_REPOS_DIR:-/home/slin/repos}:/repos:ro - ./data:/app/data:ro # Optional env_file (required: false): a missing .env.watchdog must NOT fail # `docker compose up` for the prod orchestrator (self-hosting safety). Absent # file -> WATCHDOG_* defaults, no token -> fail-safe (logs, does not send). env_file: - path: .env.watchdog required: false group_add: - "${ORCH_DOCKER_GID:-999}" # ORCH-31: staging instance (port 8501, isolated DB). # Starts ONLY with: docker compose --profile staging up -d orchestrator-staging # Normal "docker compose up -d" does NOT start this service. orchestrator-staging: profiles: - staging build: context: . args: APP_UID: ${ORCH_RUN_UID:-1000} APP_GID: ${ORCH_RUN_GID:-1000} APP_HOME: ${ORCH_AGENT_HOME_DIR:-/home/slin} container_name: orchestrator-staging restart: unless-stopped # ORCH-040: тот же uid хоста, что и у prod (см. комментарий выше / ADR-001). user: "${ORCH_RUN_UID:-1000}:${ORCH_RUN_GID:-1000}" init: true network_mode: host # ORCH-101 (D4): the same ORCH_STAGING_PORT that settings.staging_port reads — # the image_freshness rebuild target and the listening port can never drift. command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "${ORCH_STAGING_PORT:-8501}"] volumes: - ./data/staging:/app/data - ${ORCH_HOST_REPOS_DIR:-/home/slin/repos}:/repos - /var/run/docker.sock:/var/run/docker.sock - ${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:-/home/slin/.claude}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.claude - ${ORCH_HOST_CLAUDE_JSON:-/home/slin/.claude.json}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.claude.json:ro # ORCH-040: target согласован с HOME (settings.agent_home_dir), не /root/.ssh. - ${ORCH_HOST_SSH_DIR:-/home/slin/.orchestrator-ssh}:${ORCH_AGENT_HOME_DIR:-/home/slin}/.ssh:ro env_file: .env.staging environment: - ORCH_REPOS_DIR=/repos - ORCH_HOST_REPOS_DIR=${ORCH_HOST_REPOS_DIR:-/home/slin/repos} - DEPLOY_SSH_USER=${ORCH_DEPLOY_SSH_USER:-slin} - DEPLOY_SSH_HOST=127.0.0.1 - DEPLOY_HOOK_SCRIPT=${DEPLOY_HOOK_SCRIPT:-/home/slin/bin/enduro-deploy-hook.sh} # Staging DB is isolated via ./data/staging volume mount. # Inside the container the path remains /app/data/orchestrator.db (same default), # but on the host it physically lives at ./data/staging/orchestrator.db — # completely separate from prod ./data/orchestrator.db. - ORCH_DB_PATH=/app/data/orchestrator.db group_add: - "${ORCH_DOCKER_GID:-999}"