Files
obsidian-vault/personal/tech/watchtower-docker-updates.md
T

238 lines
17 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Watchtower — почему не обновляет контейнеры
created: '2026-09-20'
updated: '2026-09-27'
type: tech
namespace: personal
tags: [watchtower, docker, updates, infra, pitfall, compose]
related:
- "[[family/how-to/kraken-watchtower]]"
- "[[personal/tech/docker-data-root-mount-race]]"
- "[[personal/tech/jellyfin-db-schema-mismatch]]"
- "[[personal/tech/kraken-docker-startup-order]]"
---
# Watchtower — почему не обновляет контейнеры
Реюзабельная теория. Живой разбор на Kraken — в [[family/how-to/kraken-watchtower]].
**Главный вывод: `docker ps` показывает `Up (healthy)` — это ничего не говорит о том, что Watchtower хоть раз что-то обновил.** Отсутствие обновлений не даёт ни ошибки, ни алерта. На Kraken Watchtower «работал» 2.5 месяца и не обновил ни один из 12 контейнеров.
## Четыре причины (проверять в этом порядке)
### 1. Не задан `--interval` → таймер сбрасывается каждым стартом
Без флага интервал = 24ч **от момента старта процесса**. Любой рестарт контейнера (reboot, `docker restart`, обновление самого Watchtower) обнуляет отсчёт. Если контейнер рестартуют чаще раза в сутки — **проверка не наступает никогда**.
Признак в логе:
```
level=info msg="Scheduling first run: ... in 23 hours, 59 minutes, 59 seconds"
level=info msg="Waiting for running update to be finished..."
```
Вторая строка — процесс проснулся, увидел незавершённый апдейт и **снова уснул**. Признак, что цикл заклинило.
**Фикс:** `--interval 86400` явно, либо `--schedule "0 0 4 * * *"` (cron-синтаксис из 6 полей), если нужен запуск в тихое время.
### 2. `DOCKER_API_VERSION` запинен на старую версию
`DOCKER_API_VERSION=1.40` в environment **ломает** чтение метаданных образа. Симптом:
```
level=info msg="Found new lscr.io/linuxserver/prowlarr:latest image (3e9bd62ca90c)"
level=info msg="Unable to update container \"/prowlarr\": no available image info. Proceeding to next."
```
Watchtower **нашёл** новый образ, но не может обновиться. В новых версиях API показывается сам: `using Docker API v1.54`.
**Фикс:** убрать `DOCKER_API_VERSION` из environment вообще. Watchtower сам согласует версию с dockerd.
### 3. Образ `containrrr/watchtower` не развивается
Last push `containrrr/watchtower` — **2023-11-11** (проверено Docker Hub API). Он несовместим с современным dockerd, отсюда и `no available image info` — это не только про API-версию.
**Фикс:** активный форк `nickfedor/watchtower` (актуальная версия 1.22.2, теги `latest`, `nightly`, semver).
```bash
# проверить дату образа
curl -s "https://hub.docker.com/v2/repositories/containrrr/watchtower/tags?page_size=5" \
| jq -r '.results[] | "\(.name) updated=\(.last_updated)"'
# проверить поддержку своей архитектуры (например arm64 для RPi)
curl -s "https://hub.docker.com/v2/repositories/nickfedor/watchtower/tags/1.22.2" \
| jq -r '.images[] | "\(.architecture) \(.os)"'
```
### 4. `--label-enable` без единого label → не обновит НИЧЕГО
С флагом `--label-enable` Watchtower трогает только контейнеры с label
`com.centurylinklabs.watchtower.enable=true`. Если label нет ни у кого — **сам по себе флаг делает Watchtower бесполезным**.
Проверка, кто помечен:
```bash
for c in $(docker ps --format "{{.Names}}"); do
echo "$c -> $(docker inspect $c -f '{{index .Config.Labels "com.centurylinklabs.watchtower.enable"}}')"
done
```
⚠️ `docker inspect` на контейнере без label вернёт **пустую строку**, а не «false» — не путать с `false` (явное отключение).
## Рабочий конфиг
```yaml
services:
watchtower:
image: nickfedor/watchtower:1.22.2
container_name: watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 86400 --cleanup --label-enable
# НЕ добавлять DOCKER_API_VERSION
```
- `--interval 86400` — сутки, видно в логе рядом со `Next scheduled run`
- `--cleanup` — удалять старые образы; без него диск забивается (на Kraken 87%)
- `--label-enable` — оптин: обновляется только то, что явно помечено
## 🔴 Причина 5 (главная по коварству): правка compose НЕ ПРИМЕНЕНА
`--interval`, дедуп по API-версии, форк образа — всё это может быть **правильно прописано в файле**
и при этом **не действовать**, потому что контейнер не пересоздан. Compose-файл — **декларация**;
работающая конфигурация меняется только после `docker compose up -d`.
**Маркер проблемы:** конфиг в файле ≠ `docker inspect` контейнера.
```bash
# что РЕАЛЬНО запущено (а не что написано в файле)
docker inspect watchtower --format 'Image:{{.Config.Image}}'
docker inspect watchtower --format 'Cmd:{{.Config.Cmd}}'
docker inspect watchtower --format 'Created:{{.Created}}'
```
**Признак, что правка не применена:** `Created` (дата создания контейнера) **старше** даты правки
compose-файла. На Kraken: файл исправлен 2026-09-20, контейнер создан 2026-05-12 и не менялся —
4 месяца крутился старый `containrrr/watchtower` с `Cmd: []`.
### 🔴 Почему фейковая проверка проходит: `docker run --rm` проверяет ОБРАЗ, а не контейнер
Соблазнительный способ «проверить Watchtower»:
```bash
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
nickfedor/watchtower:1.22.2 --run-once --monitor-only --label-enable
# → Update session completed failed=0 scanned=7 updated=0 ← выглядит как доказательство!
```
Этот прогон создаёт **временный контейнер из указанного образа** и корректно отвечает на вопрос
«рабочий ли образ `nickfedor/watchtower:1.22.2`». Он **ничего не говорит** о том, какой образ
и с какими флагами крутится в боевом контейнере `watchtower`. На Kraken именно этот прогон был
записан в документацию как «проверено, фикс работает» — при том, что боевой Watchtower оставался
старым и **убивал контейнеры**.
🔴 **Правило:** для проверки **работающего** сервиса смотреть `docker inspect <container>`,
а не запускать отдельный экземпляр образа. `docker run --rm` = проверка образа.
## 🔴 Watchtower может УБИВАТЬ контейнеры (SIGTERM → SIGKILL)
Watchtower не только обновляет — он **останавливает** контейнеры, и это видно как:
```
container kill signal=2 ← SIGTERM
container kill signal=9 ← SIGKILL, пауза ~10 с = таймаут docker stop
container die exitCode=137
```
`exitCode=137` = 128 + 9 (SIGKILL). **Не путать с OOM-killer:** проверить
`dmesg | grep -i -E 'oom|killed process'` и `journalctl -k -b` — если пусто, убийца не ядро.
⚠️ **`restart: unless-stopped` НЕ защищает от Watchtower.** Политики рестарта действуют на сбои;
Watchtower останавливает контейнер сам, штатно, чтобы пересоздать его с новым образом.
Признак в логе Watchtower, что он полез вне расписания:
```
level=info msg="Waiting for running update to be finished..."
```
Это значит предыдущая сессия обновления зависла и не закрылась — Watchtower дождался её
и пошёл по контейнерам немедленно.
**Как исключить контейнер из-под Watchtower:** метка `com.centurylinklabs.watchtower.enable=false`
(требуется `--label-enable`, иначе метка игнорируется).
## 🔴 `labels` нельзя изменить у работающего контейнера
**`docker update --label-add` НЕ СУЩЕСТВУЕТ** (`unknown flag: --label-add`). Метки задаются
только при создании контейнера. Единственный путь: правка `labels` в compose + `docker compose up -d`
(пересоздание). Проверено на Docker 29.4.3 / Compose v5.1.3.
Практическое следствие: «проставить метку без пересоздания» — нереализуемая идея. Пересоздание
безопасно, если данные в томах, но требует явного решения.
## Проверка (обязательна)
`Up` + отсутствие ошибок ≠ работа. **И `docker run --rm` ≠ проверка работающего сервиса** (см. выше).
Правильный двухшаговый порядок:
```bash
# ШАГ 1 — что РЕАЛЬНО запущено (главная проверка)
docker inspect watchtower --format 'Image:{{.Config.Image}} Cmd:{{.Config.Cmd}} Created:{{.Created}}'
# Image: nickfedor/watchtower:1.22.2
# Cmd: [--interval 86400 --cleanup --label-enable]
# Created: <должна быть ПОЗЖЕ правки compose-файла>
# ШАГ 2 — рабочий ли образ (вспомогательно)
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
nickfedor/watchtower:1.22.2 --run-once --monitor-only --label-enable
```
`--monitor-only` обязателен, иначе `--run-once` **реально обновит** контейнеры.
Что должно быть в выводе:
```
level=info msg="Watchtower 1.22.2 using Docker API v1.54"
level=info msg="Found new image" container=<X> new_id=<...>
level=info msg="Update session completed" failed=0 scanned=N skipped=0 updated=0
```
`Found new image` = баг с метаданными вылечен. `updated=0` в monitor-only — нормально.
⚠️ Если `scanned` **больше** ожидаемого числа размеченных контейнеров — значит `--label-enable`
не действует, и Watchtower трогает всё подряд. Проверять `Cmd` из шага 1.
## ⚠️ Автообновление может сломать сервис: пинить мажор версии
Тип `latest` + автообновление = переход на новую **мажорную** версию без предупреждения. Для сервисов с миграциями БД (EF Core, Prisma, любые forward-only миграции) это необратимо:
- Jellyfin на Kraken: `latest` уехал 10.11.8 → 12.1, БД мигрировала вперёд, откатить схему нельзя → сервер отдавал **503**, `no such column: ...`. Детали: [[personal/tech/jellyfin-db-schema-mismatch]].
**Правило (общее):** сервисам с forward-only миграциями БД безопаснее пиновый тег (`image: jellyfin/jellyfin:12.1`), а не `latest` + автообновление.
⚠️ **Но это рекомендация, не аксиома — на Kraken решено иначе.** Alex 2026-09-20 явно выбрал `latest` для Jellyfin: «пусть ставит до всех публичных релизов», т.е. включая мажор 13. Риск принят осознанно, бэкап БД делается перед пересозданием. См. [[family/how-to/kraken-watchtower]] § «Про Jellyfin и тег».
Отдельно важный нюанс: **упал не мажорный апдейт как таковой**, а рассинхрон «контейнер на старом образе + уже мигрированная БД». Сама миграция прошла нормально. То есть мажорный апдейт БД не ломает — ломает откат сервера назад без отката схемы.
## Питфоллы диагностики
- `docker logs watchtower` — Watchtower пишет в **stdout**, в отличие от Jellyfin (тот пишет в файл). Смотреть логи легко.
- Таймстемпы в логе Watchtower — **UTC**, локальное время сервиса может быть другим.
- Единожды увиденный `Found new image` не значит, что обновление применилось — нужна следующая строка (`Unable to update` / `Updated`).
- Проверять надо **фактическую** дату запущенного образа, а не тег:
```bash
docker inspect <container> -f '{{index .Config.Labels "org.opencontainers.image.version"}}'
docker inspect <container> -f '{{index .Config.Labels "build_version"}}'
```
## Урок
Watchtower — компонент, который **молча не работает**. Его нельзя «поставить и забыть»: обязательны (а) явный `--interval`, (б) отсутствие `DOCKER_API_VERSION`, (в) периодический `--run-once --monitor-only` как health-check, (г) осознанное решение, каким контейнерам вообще разрешено автообновление, (д) **проверка `docker inspect`, что правка реально применена**.
### 🔴 Два универсальных урока (2026-09-27)
**1. Файл ≠ работающая конфигурация.** Compose-файл — декларация. Пока не выполнен
`docker compose up -d`, ничего не изменилось. Проверять **`docker inspect` контейнера**, а не файл.
Дата создания контейнера (`Created`) старше даты правки файла = **правка не применена**.
**2. `docker run --rm <образ>` проверяет ОБРАЗ, а не работающий контейнер.** Прогон временного
контейнера из «правильного» образа выглядит как полноценное доказательство (`scanned=7 failed=0`),
но не говорит ничего о боевом сервисе. Именно так на Kraken в документацию попала запись
«фикс выполнен», при том что 4 месяца крутился старый Watchtower и **убивал контейнеры**.
Для проверки боевого сервиса — только `docker inspect` самого контейнера.
Оба урока общие, не только про Watchtower: они применимы к любому сервису, правка которого
делается через compose-файл.
## Связанные
- [[family/how-to/kraken-watchtower]] — живой разбор и точные команды на Kraken
- [[personal/tech/kraken-docker-startup-order]] — что Watchtower ломает на Kraken (убийство контейнеров, exit 137)
- [[personal/tech/jellyfin-db-schema-mismatch]] — что ломает автообновление мажора
- [[personal/tech/docker-data-root-mount-race]] — другой дефект «контейнеры пропали»