- task image принимает BUILD_ID и тегает <app>:$BUILD_ID (контракт роли app_image в umbar) - добавлен ADR-2026-07-24-local-image-build, старый docker-deploy помечен superseded - README/architecture/roadmap/Dockerfile обновлены под новую схему (без сборки на сервере)
5.6 KiB
Образ собирается локально и едет на сервер через docker save/load
- Дата: 2026-07-24
Контекст
Заменяет ADR-2026-06-13-docker-deploy.
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из окружения и собирает полный образ<app>:$BUILD_ID. БезBUILD_IDсобирается<app>:dev— обратная совместимость для локальной работы. Приложение полностью владеет тем, как собирается его образ (Dockerfile— в репозитории приложения). - Сборка локальна. Роль генерит случайный
BUILD_IDи гоняет с нимtask imageна control-хосте → образ<app>:$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/loadtar).+Механизм общий (рольapp_image), а не вписан в один плейбук — им же доставляется trackers.-Дедупликации нет: тег = случайныйBUILD_ID, поэтому каждый деплой везёт образ и пересоздаёт контейнер, даже если ничего не менялось. Для ручного нечастого деплоя это осознанный размен — простота роли важнее экономии одного рестарта.-save/loadвезёт весь образ (базовый слой + бинарь), а не только бинарь, как в прежней схеме. Дляdistroless/staticэто единицы мегабайт — дёшево.