238 lines
17 KiB
Markdown
Executable File
238 lines
17 KiB
Markdown
Executable File
---
|
||
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]] — другой дефект «контейнеры пропали»
|