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

# adr-0025: Build-cache-pruner — фоновый heartbeat-демон авто-уборки docker build cache на хосте

> Сквозной (cross-cutting) ADR: вводит **новый фоновый компонент** оркестратора в ряду
> `reconciler` (adr-0007), `job_reaper` (adr-0011) и `disk_watchdog` (adr-0024). Детальное
> решение задачи — `docs/work-items/ORCH-062/06-adr/ADR-001-build-cache-pruner.md`.

## Статус
Proposed (ORCH-062)

## Контекст

07.06.2026 диск хоста mva154 тихо дорос до 100% и положил **весь self-hosting-конвейер всех
проектов** (один прод-инстанс `orchestrator` на общей БД/очереди). Доминирующий «пожиратель» —
**docker build cache** (≈11 ГБ от частых пересборок прод/staging-образов). `disk_watchdog`
(adr-0024, ORCH-063) ввёл **сигнал** о заполнении (Telegram ≥85%) и явно отложил авто-очистку в
отдельную задачу. ORCH-062 — эта задача: **автоматическое освобождение build cache**, чтобы
инцидент не повторялся без оператора.

Сверено по коду: контейнер `orchestrator` **не содержит docker CLI** (`Dockerfile:11` — только
`openssh-client git curl`); host-docker-операции приложение уже делает **через ssh на хост**
(`image_freshness.image_revision`, `self_deploy` Phase B), канал `deploy_ssh_user@deploy_ssh_host`
настроен. У оркестратора три проверенных фоновых daemon-потока с единым каркасом.

## Решение

Вводится четвёртый фоновый компонент **build-cache-pruner** (`src/build_cache_pruner.py`):
- **Калька каркаса** `disk_watchdog`/`reconciler`/`reaper`: daemon-поток, чистый стоп через
  `_stop.wait(interval)`, контракт `start()`/`stop(timeout)`/`status()`, старт/стоп в
  `main.lifespan` (старт последним — после `disk_watchdog.start()`; стоп первым в reverse),
  наблюдаемость — аддитивный блок `build_cache_prune` в `GET /queue`. Leaf-модуль (без обратных
  зависимостей на `stage_engine`/`stages`/`qg`).
- **Уборка — строго `docker builder prune -f --filter until=<until>`** (BuildKit GC, дефолт
  `until=24h`): удаляется только старый build cache, тёплый ≤24ч сохраняется. `-a` — опционально и
  только в паре с возрастным фильтром. **Запрещены** `docker image prune`/`system prune`/удаление
  образов запущенных сервисов/остановка-рестарт контейнеров.
- **Исполнение на хосте через ssh** (CLI в контейнере нет): `ssh deploy_ssh_user@deploy_ssh_host
  "docker builder prune …"`, bounded таймаутом. **Нет ssh-таргета → тик no-op** → фича
  естественно скоупится на self-hosting-прод.
- **Конфиг/kill-switch** (`ORCH_BUILD_CACHE_PRUNE_*`, дефолты безопасные): `enabled` (дефолт
  `true`), `interval_s` (6ч), `until` (`24h`), `all` (`false`), `timeout_s`, `notify_min_gb`.
  Валидаторы по образцу `disk_monitor_*` (невалид → лог + дефолт).
- **Сигнал + лечение как пара:** disk_watchdog сигналит о росте диска, build-cache-pruner убирает
  доминирующего «пожирателя» — две половины одной операционной защиты.

**Инварианты:** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, `src/stage_engine.py`, схема БД
— **не меняются** (pruner — эксплуатационный демон, не Quality Gate, как watchdog/reaper). Без
миграции БД (учёт результата in-memory, best-effort). never-raise per-команда/per-tick. Уборка
**никогда** не рестартит docker daemon/прод-контейнер (self-hosting безопасность; рестарт-путь —
отвергнутый Вариант B). При выключенном kill-switch — поведение 1:1 как сейчас (нулевая регрессия
для enduro-trails).

## Альтернативы
- **host `daemon.json builder.gc.defaultKeepStorage`** — отвергнуто: требует рестарта docker
  daemon (останавливает ВСЕ контейнеры хоста = групповой self-hosting риск); политика по объёму,
  не по возрасту; не наблюдаемо в `GET /queue`.
- **host-cron** — отвергнуто как основное (оставлено ручным fallback): off-git невидимая инфра,
  без `/queue`-наблюдаемости, без config-kill-switch, не тестируется.
- **raw-HTTP по docker.sock / docker CLI в образе** — отвергнуто: лишний код / раздувание образа
  против уже существующего ssh-канала.

## Последствия
- **+** Корень инцидента 07.06 устраняется автоматически; тёплый кэш сохранён; без новых
  зависимостей и без рестарта docker/прода (принцип «всё в Docker, минимум зависимостей»).
- **+** Знакомый паттерн фонового демона → низкий риск, наблюдаемость, обратимость, тестируемость.
- **−** Зависимость от ssh на хост (как `image_freshness`/`self_deploy`); нет таргета → no-op
  (наблюдаемо), фича не работает, но ничего не ломает.
- **Откат:** `ORCH_BUILD_CACHE_PRUNE_ENABLED=false`; миграций БД нет.

## Ссылки
- Задачный ADR: `docs/work-items/ORCH-062/06-adr/ADR-001-build-cache-pruner.md`
- Инфра/риски: `docs/work-items/ORCH-062/07-infra-requirements.md`,
  `docs/work-items/ORCH-062/10-tech-risks.md`
- Комплемент: [adr-0024-disk-watchdog.md](adr-0024-disk-watchdog.md) (ORCH-063 — сигнал)
- Родственные компоненты: [adr-0007-reconciler.md](adr-0007-reconciler.md),
  [adr-0011-job-reaper-lease-reclaim.md](adr-0011-job-reaper-lease-reclaim.md)
- Топология host / env-карта: `docs/operations/INFRA.md`
</content>
