# Образ собирается локально и едет на сервер через docker save/load - Дата: 2026-07-24 ## Контекст Заменяет [ADR-2026-06-13-docker-deploy](ADR-2026-06-13-docker-deploy.md). Docker как единица деплоя и распределение ответственности (`Dockerfile` — упаковка — живёт в jellybit; оркестрация — в umbar) остаются в силе. Пересматривается только **как** образ попадает на сервер. Прежняя схема собирала статический бинарь на control-хосте, копировала на сервер бинарь + `Dockerfile` и делала `docker build` **на месте**. У этого два неудобства. Во-первых, на сервере остаётся шаг сборки: пусть дешёвый на Intel N150, но результат косвенно завязан на состояние сервера (его docker, его кэш), а не только на исходник. Во-вторых, схема одноразовая — она была вписана в `playbook-jellybit.yml` и не переиспользовалась. Появление второго такого же приложения (trackers) потребовало вынести доставку образа в общую ansible-роль `app_image` и заодно унифицировать контракт с приложением. ## Рассмотренные варианты - **Оставить сборку на сервере** — прежнее решение. Держит на сервере шаг `docker build` и делает результат зависимым от состояния сервера; переиспользовать без копипасты плейбука неудобно. - **Реестр (CI пушит образ, сервер тянет)** — каноничнее, но в домашней лаборатории это лишний реестр и пайплайн ради одного узла. Отвергнуто по той же причине, что и в исходной ADR. - **Собрать полный образ локально и доставить его `docker save`/`load`** — выбрано: сборка целиком на control-хосте, сервер — только получатель, и реестр не нужен. ## Решение Доставку образа ведёт переиспользуемая роль `app_image` в umbar. Схема: - **Контракт с приложением.** Приложение реализует команду `task image`: получает `BUILD_ID` из окружения и собирает **полный** образ `:$BUILD_ID`. Без `BUILD_ID` собирается `:dev` — обратная совместимость для локальной работы. Приложение полностью владеет тем, как собирается его образ (`Dockerfile` — в репозитории приложения). - **Сборка локальна.** Роль генерит случайный `BUILD_ID` и гоняет с ним `task image` на control-хосте → образ `:$BUILD_ID`. На сервере Go-тулчейн и `docker build` больше не нужны. - **Тег = BUILD_ID.** Deploy-тег — тот самый случайный `BUILD_ID`. Дедупликации по содержимому нет: тег нов на каждый прогон. Content-адресацию (тег из хеша слоёв/конфига образа) рассматривали и отвергли — на сценариях ручного нечастого деплоя выгода от неё не окупала сложности. - **Доставка без реестра.** `docker save` → copy tar → `docker load`. Каждый деплой везёт образ и пересоздаёт контейнер. - **Уборка.** Старые образы на сервере (тег каждого прошлого деплоя) подчищает `docker image prune -af` по крону (`playbook-system.yml`). ## Последствия - `+` Сервер — только получатель образа: без Go-тулчейна и без шага `docker build`. - `+` Сборка целиком на control-хосте — воспроизводимее; состояние сервера на результат не влияет. - `+` Реестр по-прежнему не нужен — доставка дешёвая (`save`/`load` tar). - `+` Механизм общий (роль `app_image`), а не вписан в один плейбук — им же доставляется trackers. - `-` Дедупликации нет: тег = случайный `BUILD_ID`, поэтому каждый деплой везёт образ и пересоздаёт контейнер, даже если ничего не менялось. Для ручного нечастого деплоя это осознанный размен — простота роли важнее экономии одного рестарта. - `-` `save`/`load` везёт весь образ (базовый слой + бинарь), а не только бинарь, как в прежней схеме. Для `distroless/static` это единицы мегабайт — дёшево.