---
work_item: ORCH-123
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-16
model_used: claude-opus-4-8
---

# adr-0049: Граница исполнения docker — все docker-операции host-side, не изнутри app-контейнера

> **Сквозной (cross-cutting) ADR.** Кодифицирует инвариант **«docker-операции оркестратора
> исполняются host-side через доверенный ssh-канал, никогда изнутри прод-контейнера»**, охватывающий
> компоненты ORCH-036/058/115/123/101, и **амендит** execution-strategy-решение
> [adr-0048](adr-0048-deterministic-staging-runner.md) (D3/D5). Поводом стала задача ORCH-123 (баг:
> staging-runner отклонился от инварианта). Локальная детализация (D1–D9) —
> `docs/work-items/ORCH-123/06-adr/ADR-001-host-side-staging-execution-and-env-classification.md`.

## Статус
Proposed

## Контекст

Прод-контейнер `orchestrator` (8500) **не содержит docker CLI** (`Dockerfile:11`:
`openssh-client git curl ca-certificates` + pinned gitleaks; `python:3.12-slim` docker не несёт).
`/var/run/docker.sock` смонтирован rw + `group_add 999` (ORCH-040 «МИНА 1»), но **клиента, который
бы им воспользовался, нет** — сознательно: добавление CLI/SDK активировало бы root-эквивалентный путь
исполнения для всего, что бежит в контейнере (вкл. LLM-агентов). Поэтому в оркестраторе сложился
**инвариант исполнения**, ранее не выделенный в отдельный ADR:

- **ORCH-036** (`self_deploy.build_deploy_command`, [adr-0007](adr-0007-executable-self-deploy.md)) —
  прод-деплой исполняется host-side через `ssh + setsid bash <hook> --deploy` на `127.0.0.1`.
- **ORCH-058** (`image_freshness`, [adr-0008](adr-0008-staging-image-provenance.md)) — ребилд
  staging-образа (`ssh … bash <hook> --build-staging`) и инспекция revision
  (`image_revision(ssh_target=…)`) — host-side; модуль прямо документирует:
  *«docker lives on the HOST (the container ships only openssh-client git)»*.
- **ORCH-101** ([adr-0036](adr-0036-replication-foundation-host-parametrization.md)) — host-параметры
  канала (`deploy_ssh_*`, `deploy_host_repo_path`, `repos_dir`/`host_repos_dir`) расхардкожены.

**ORCH-115** ([adr-0048](adr-0048-deterministic-staging-runner.md)), заменяя LLM-деплойера
детерминированным `staging_runner`, **отклонился** от инварианта: зашил `docker exec` **изнутри**
прод-контейнера через `proc_group → Popen` → `FileNotFoundError: docker` → постоянный
environment-дефект, ложно маршрутизированный как транзиентная инфра → DEFER → fail-closed FAILED →
**откат `deploy-staging → development`** (винит код задачи за дефект окружения раннера). Инцидент
ORCH-116/ORCH-123.

## Решение

**Кодифицировать инвариант (нормативно):** docker-операции оркестратора (`docker`/`docker compose`/
`docker exec`/`docker inspect`/`docker tag`) исполняются **host-side** через доверенный ssh-канал
(`deploy_ssh_host=127.0.0.1`, ключ смонтирован, `openssh-client` в образе) — **никогда** изнутри
прод-контейнера, который docker CLI не несёт. `/var/run/docker.sock` **не используется** изнутри
контейнера; docker CLI/SDK в образ **не добавляется** (любое исключение — отдельный явный
security-review: socket-из-контейнера = root-эквивалент на хосте, обслуживающем все проекты).

**ORCH-123 приводит `staging_runner` в соответствие** (амендит adr-0048 D3/D5):
- **D3 (амендмент adr-0048):** `staging_runner.build_staging_command` теперь обёртывает
  `docker exec orchestrator-staging python3 staging_check.py …` в `ssh <user>@<host> '<…>'` (зеркало
  `image_freshness.image_revision(ssh_target=…)`). Внутренняя команда сюиты и exit-код-контракт — те
  же; меняется лишь **инициатор/канал**.
- **D5 (амендмент adr-0048 двухуровневого исхода):** введён **третий** класс исхода `permanent-env`
  (зеркало `merge_gate.classify_retest_failure`, ORCH-110); корневой инвариант — **«сюита не
  исполнилась» (environment ИЛИ транзиентная инфра) НИКОГДА не оканчивается код-фейл-откатом и не жжёт
  developer-retry**; откат — только для реально исполнившейся сюиты с `exit≠0`. Терминал исчерпания
  DEFER изменён с fail-closed-FAILED+advance на **infra-HOLD + alert** (как ORCH-110 D3).

Кросс-каттинговые инварианты (сохранены **байт-в-байт**, как adr-0048):
- `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_staging_status`/`_parse_staging_status` /
  machine-verdict-ключи (`staging_status:`/`deploy_status:`/…) / **схема БД** — не тронуты (замена
  *стратегии исполнения продюсера*, не гейта/стадии).
- Единственный транспорт LLM-консультации (`launcher._spawn`/S0, [adr-0047](adr-0047-llm-usage-policy-and-call-site-map.md))
  — соблюдён (раннер LLM не зовёт).
- Сквозной бюджет времени ORCH-065/109/110 (`reaper_max_running_s` > Σ(работ на ребре) + grace) — не
  растёт (host-side ssh заменяет in-container call, окно ≤ `staging_runner_timeout_s`).
- Граница transition-lease ORCH-114 — берётся внутри `advance_stage`; раннер не трогает.

Скоуп — **self-hosting only** (`staging_runner_repos=""` → `is_self_hosting_repo`); под флагами
`staging_runner_enabled` (→ LLM-путь) и **новым** `staging_runner_exec_host_side` (дефолт `True` →
фикс; `False` → прежний in-container call). never-raise во всех публичных функциях.

## Последствия

- **+** Инвариант «docker host-side» выделен и задокументирован → будущие компоненты не повторят
  отклонение ORCH-115; reviewer ловит in-container docker как регресс инварианта.
- **+** staging-сюита реально исполняется в проде; инфра/environment ≠ код-фейл на staging-ребре
  (закрыт RCA-класс ORCH-110 на этом ребре полностью); анти-over-tolerance цел.
- **+** Без расширения привилегий (нет docker CLI/SDK в контейнере, сокет не используется); согласовано
  с ORCH-036/058.
- **−** Remote tree-kill ограничен локальным ssh-клиентом (как `image_freshness.rebuild_staging_image`);
  backstop — bounded таймаут внутри `staging_check.py`.
- **−** Permanent-env/исчерпавшая-DEFER задача держится на `deploy-staging` (блокирует serial-gate репо
  до починки оператором) — принятый tradeoff (зеркало ORCH-110), self-hosting only.
- **Откат:** `ORCH_STAGING_RUNNER_ENABLED=false` (→ LLM) или `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=false`
  (→ in-container call).

## Ссылки
- Локальный ADR: `docs/work-items/ORCH-123/06-adr/ADR-001-host-side-staging-execution-and-env-classification.md`
- Амендит: [adr-0048](adr-0048-deterministic-staging-runner.md) (D3/D5 ORCH-115)
- Опирается на: [adr-0007](adr-0007-executable-self-deploy.md) (ORCH-036 self-deploy ssh),
  [adr-0008](adr-0008-staging-image-provenance.md) (ORCH-058 image-freshness host-side docker),
  [adr-0042](adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md) (ORCH-110 proc_group +
  classify + infra-tolerance), [adr-0036](adr-0036-replication-foundation-host-parametrization.md)
  (ORCH-101 host-параметризация)
- Сверено по коду: `src/staging_runner.py`, `src/self_deploy.py:220`, `src/image_freshness.py:185/246`,
  `scripts/orchestrator-deploy-hook.sh:166/197`, `Dockerfile:11`, `docker-compose.yml`
