diff --git a/Dockerfile b/Dockerfile index 7fa9340..4b0c213 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ -# Упаковка готового статического бинаря в минимальный образ. -# Бинарь собирается снаружи (см. docs/adr/ADR-2026-06-13-docker-deploy.md): +# Упаковка готового статического бинаря в минимальный образ. Образ целиком +# собирается локально на control-хосте (`task image`) и едет на сервер через +# docker save/load — роль app_image в umbar (см. +# docs/adr/ADR-2026-07-24-local-image-build.md). +# Бинарь собирается снаружи: # CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o jellybit ./cmd/jellybit # distroless/static несёт CA-сертификаты (HTTPS к LLM/TMDB). Пользователь # задаётся в compose (user: "1000:1000"). diff --git a/README.md b/README.md index 3ea10fb..3a9e597 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,11 @@ jellybit recognize --dry-run [--context "..."] --config ./config.toml Рассчитан на домашний медиа-сервер. Артефакты репозитория — статический бинарь (`task build`) и `Dockerfile` (упаковка в `distroless/static`). Образ -собирается **на сервере** из доставленного бинаря, поэтому Go-тулчейн на -сервере не нужен. В distroless нет shell/curl, поэтому HEALTHCHECK зовёт сам -бинарь: `jellybit healthcheck` (GET `/healthz` по порту из конфига, exit 0/1). +собирается целиком **локально** на control-хосте (`task image`) и едет на +сервер через `docker save`/`load` (роль `app_image` в umbar), поэтому +Go-тулчейн и `docker build` на сервере не нужны. В distroless нет shell/curl, +поэтому HEALTHCHECK зовёт сам бинарь: `jellybit healthcheck` (GET `/healthz` +по порту из конфига, exit 0/1). Контейнер: `user 1000:1000`, порт `8080` на хост, mount `/srv/media` (единая песочница для хардлинков) + том `/config` (ro, `config.toml`, восстановим при diff --git a/Taskfile.yml b/Taskfile.yml index d1d9102..e16821c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -76,10 +76,10 @@ tasks: done < web/assets.manifest image: - desc: Docker-образ из готового бинаря (см. docs/adr docker-deploy) + desc: 'Docker-образ из готового бинаря. Тег из $BUILD_ID (по умолчанию dev; роль app_image umbar передаёт свой). См. docs/adr local-image-build' deps: [build] cmds: - - docker build -t jellybit:dev . + - docker build -t {{.BINARY}}:${BUILD_ID:-dev} . clean: desc: Удалить собранный бинарь diff --git a/docs/adr/ADR-2026-06-13-docker-deploy.md b/docs/adr/ADR-2026-06-13-docker-deploy.md index e692a81..873d214 100644 --- a/docs/adr/ADR-2026-06-13-docker-deploy.md +++ b/docs/adr/ADR-2026-06-13-docker-deploy.md @@ -1,6 +1,7 @@ # Docker как единица деплоя, образ собирается на сервере - Дата: 2026-06-13 +- Статус: заменено на ADR-2026-07-24-local-image-build ## Контекст diff --git a/docs/adr/ADR-2026-07-24-local-image-build.md b/docs/adr/ADR-2026-07-24-local-image-build.md new file mode 100644 index 0000000..457894a --- /dev/null +++ b/docs/adr/ADR-2026-07-24-local-image-build.md @@ -0,0 +1,71 @@ +# Образ собирается локально и едет на сервер через 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` это единицы + мегабайт — дёшево. diff --git a/docs/adr/README.md b/docs/adr/README.md index bdfe918..72c4a46 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -56,8 +56,9 @@ | Дата | Запись | Статус | | ---------- | ---------------------------------------------------------------- | ------ | +| 2026-07-24 | [Локальная сборка образа + доставка docker save/load](ADR-2026-07-24-local-image-build.md) | — | | 2026-07-23 | [Конвейер ревью: гейт, generative-проходы и триаж](ADR-2026-07-23-review-pipeline-generative.md) | — | | 2026-06-13 | [Авто-раскладка только при матче в метабазе](ADR-2026-06-13-auto-link-requires-db-match.md) | — | -| 2026-06-13 | [Docker как единица деплоя](ADR-2026-06-13-docker-deploy.md) | — | +| 2026-06-13 | [Docker как единица деплоя](ADR-2026-06-13-docker-deploy.md) | заменено на ADR-2026-07-24-local-image-build | | 2026-06-13 | [Хардлинки вместо копирования и симлинков](ADR-2026-06-13-hardlinks.md) | — | | 2026-06-13 | [Go и доставка одним бинарём](ADR-2026-06-13-go-single-binary.md) | — | diff --git a/docs/drafts/roadmap.md b/docs/drafts/roadmap.md index 1e54f34..b59d885 100644 --- a/docs/drafts/roadmap.md +++ b/docs/drafts/roadmap.md @@ -28,9 +28,11 @@ подтверждение в боте (карточка + кнопки + reply-подсказка, эскалация в веб), полный редактор маппинга «файл → серия», триггер скана Jellyfin, нотификации. -- **Ф6 — деплой.** Сборка статического бинаря здесь; доставка бинаря + - `Dockerfile` на сервер, `docker build` и запуск на месте; оркестрация — - `playbook-jellybit.yml` в umbar: общая docker-сеть, `user 1000:1000`, +- **Ф6 — деплой.** Полный образ собирается локально на control-хосте + (`task image`) и едет на сервер через `docker save`/`load` (роль + `app_image` в umbar), там и запускается — `docker build` на сервере нет; + оркестрация — `playbook-jellybit.yml` в umbar: общая docker-сеть, + `user 1000:1000`, mount `/srv/media` + data-том `/srv/applications/jellybit/data`, healthcheck. Сопутствующие правки qBit (том `/srv/media`, savepath/temp под `/srv/media`, `WebUI\ServerDomains=*`). diff --git a/docs/specs/architecture.md b/docs/specs/architecture.md index 80d16d5..d2982d9 100644 --- a/docs/specs/architecture.md +++ b/docs/specs/architecture.md @@ -203,11 +203,12 @@ relink/лечения. Включается конфигом `[jellyfin]` (по ## Деплой Jellybit работает в **docker** — в одной среде с qBittorrent и Jellyfin -(см. [ADR-2026-06-13-docker-deploy](../adr/ADR-2026-06-13-docker-deploy.md)). +(см. [ADR-2026-07-24-local-image-build](../adr/ADR-2026-07-24-local-image-build.md)). Сборка: статический бинарь (`GOOS=linux GOARCH=amd64 CGO_ENABLED=0`, -сервер на Intel N150) собирается здесь; на сервер во временную build-папку -кладутся бинарь + `Dockerfile` (копирует бинарь в `distroless/static`), -образ собирается на месте и запускается. Go-тулчейн на сервере не нужен. +сервер на Intel N150) и **полный образ** собираются локально на control-хосте +(`task image` упаковывает бинарь в `distroless/static`). Готовый образ едет +на сервер через `docker save`/`load` (роль `app_image` в umbar), там и +запускается. Go-тулчейн и `docker build` на сервере не нужны. Параметры запуска (в umbar-compose):