把服务容器化之后,更新镜像往往成了机械劳动:定期 docker compose pull 然后 up -d。如果挂在机器上的容器多了,很容易懒得挨个去敲命令更新。

Watchtower 就是专门用来自动化这个过程的哨兵容器:它定期拉取远端镜像仓库,比对 digest 哈希,发现新版就自动拉取并按原有参数平滑重建容器。

但这玩意如果无脑开全局更新,非常容易翻车:比如数据库大版本升级导致数据结构不兼容直接挂掉,或者 upstream 镜像打了个 broken tag 把生产服务搞崩。

这篇笔记记录我生产环境的 Watchtower 跑法:只给白名单容器自动更新(label 过滤)、更新前执行备份 Hook、推送 Telegram 告警,以及配置私有镜像凭证。


全自动更新的隐患与防翻车原则

在正式把 Watchtower 投入生产之前,请必须将以下三条铁律刻入脑海:

┌────────────────────────────────────────────────────────────────────────┐
│                   Watchtower 生产级分级管控哲学                        │
├───────────────────────────────────┬────────────────────────────────────┤
│   🟢 允许自动跟更 (白名单放行)    │    🔴 严禁自动跟更 (坚决物理隔离)  │
│   • Nginx / Caddy 反向代理网关    │    • MySQL / PostgreSQL 生产数据库 │
│   • 静态博客前端 / 个人小工具    │    • Vaultwarden 私有密码保险箱    │
│   • 探针 Agent (Komari / 哪吒)    │    • 涉及复杂数据迁移的核心存储    │
│   • 无状态爬虫与聚合搜索 API     │    • 固定商业特定生产版本的核心服务 │
└───────────────────────────────────┴────────────────────────────────────┘
  1. 绝对禁止全盘无差别更新(Label Enable 是生命线):严禁自动更新有状态数据库!数据库跨大版本升级(例如 PostgreSQL 15 升至 16)会导致数据目录格式不兼容,容器重启直接死锁闪退;
  2. 拒绝旧镜像占满磁盘(Auto Cleanup):拉取新镜像后,旧镜像如果不自动清理,会变成 <none>:<none> 孤儿悬空镜像。几周时间就能吞掉几十 GB 物理硬盘,必须开启自动抹除;
  3. 滚动更新平滑切换(Rolling Restart):对于多个存在调用依赖的服务,避免同一时间全部杀掉重建,保障业务连续性。

准备工作:核心参数规划

在敲击命令前,先集中定义核心变量:

# ================= 全局参数定义 =================
WATCHTOWER_DIR="/opt/watchtower"   # 部署工作根目录
CRON_SCHEDULE="0 0 4 * * *"        # 检测计划:每天凌晨 04:00 执行一次
API_TOKEN="SecretWatchtowerToken"  # HTTP API 远程触发认证令牌
# ================================================

Watchtower 的工作原理与常见风险

1. Watchtower 如何判定“有新版”?

很多人误以为 Watchtower 会比对语义化版本号(如 1.2.0 ➔ 1.2.1),其实并非如此。
Watchtower 监听的是远程镜像仓库中该 Tag 对应的 镜像 Digest(SHA256 哈希值):
– 如果你使用的是 :latest 或动态更新的分支 Tag,只要上游开发者推送了新构建,Digest 发生改变,Watchtower 就会识别到变动;
– 如果你锁定的是固定的不可变 Tag(例如 postgres:16.1-alpine),由于该 Tag 的 Digest 从未变化,Watchtower 也绝不会主动触发升级。

2. 生产三大雷区与攻坚方案

致命雷区 ❌ 产生后果 生产攻坚方案 🟢
全量无差别更新 数据库/核心存储数据格式不兼容,业务直接瘫痪 开启 --label-enable:仅更新带有显式标签的白名单应用
废旧镜像堆积不删 每更新一次留下几百 MB 垃圾,数周后 VPS 磁盘 100% 爆死 配置 --cleanup:更新后立刻调用 Docker API 销毁旧镜像
高频轮询遭遇限流 默认每几分钟扫一次,迅速耗尽 Docker Hub 免费拉取限额报 429 采用 Cron 定时:设定在每天凌晨业务低峰期执行一次

用 Docker Compose 部署 Watchtower

这是最推荐的生产部署形态:内存占用极低(~15MB),集成了白名单过滤、自动清理废旧镜像、滚动更新与 HTTP API 远程触发能力。

服务器端:

sudo mkdir -p /opt/watchtower
cd /opt/watchtower

sudo tee /opt/watchtower/docker-compose.yml << 'EOF'
version: '3.8'

services:
  watchtower:
    image: containrrr/watchtower:latest
    container_name: watchtower
    restart: always
    environment:
      - TZ=Asia/Shanghai
      # 核心生产基线:仅更新标记了 com.centurylinklabs.watchtower.enable=true 的容器
      - WATCHTOWER_LABEL_ENABLE=true
      # 核心生产基线:更新成功后自动删除旧的废弃镜像,彻底防止塞满物理磁盘
      - WATCHTOWER_CLEANUP=true
      # 自动重启依赖:如果旧容器所属自定义网络,更新后自动重新连接
      - WATCHTOWER_INCLUDE_RESTARTING=true
      # 滚动更新控制:前一个容器完全启动并健康后再更新下一个,防止服务群瞬断
      - WATCHTOWER_ROLLING_RESTART=true
      # 每天凌晨 04:00 执行一次自动巡检 (秒 分 时 日 月 周)
      - WATCHTOWER_SCHEDULE=0 0 4 * * *
      # 日志级别:warn、info、debug
      - WATCHTOWER_LOG_LEVEL=info
      # 平滑停止超时时间:给旧容器 60 秒保存缓冲与关闭连接时间 (SIGTERM)
      - WATCHTOWER_TIMEOUT=60s
      # 核心进阶:启用 HTTP API 远程触发模式
      - WATCHTOWER_HTTP_API_UPDATE=true
      - WATCHTOWER_HTTP_API_TOKEN=SecretWatchtowerToken
      - WATCHTOWER_HTTP_API_PERIODIC=true
    volumes:
      # 挂载 Docker Socket 以便调用宿主机容器守护进程
      - /var/run/docker.sock:/var/run/docker.sock:ro
    ports:
      # 仅绑定本地回环,用于本地 CI/CD 或反代触发 HTTP API
      - "127.0.0.1:8088:8080"
    logging:
      driver: "json-file"
      options:
        max-size: "20m"
        max-file: "3"
EOF

点火启动服务

服务器端:

sudo docker compose up -d
sudo docker compose logs -f watchtower

核验标准:日志输出 Starting Watchtower,且提示定时任务与 HTTP API 已监听 :8080。


使用 Label 标签为指定容器圈定白名单

因为我们在 Watchtower 中开启了 WATCHTOWER_LABEL_ENABLE=true,默认情况下 Watchtower 会直接跳过所有未标注的容器,实现天然的物理安全隔离!

1. 允许自动更新的无状态业务(注入白名单 Label)

对于反代、探针、静态前端等轻量应用,在它的 docker-compose.yml 中添加标签:

services:
  web-frontend:
    image: nginx:alpine
    container_name: web-frontend
    # 核心:允许 Watchtower 自动更新此容器
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
    restart: always

2. 严禁自动更新的核心数据库(保持默认或显式封印)

对于 MySQL、PostgreSQL、Redis 等有状态应用,直接保持默认即可(不加标签 = 绝不触碰)。若想强化团队协作语义防止手抖:

services:
  db-postgres:
    image: postgres:16-alpine
    container_name: db-postgres
    # 显式禁止 Watchtower 管辖
    labels:
      - "com.centurylinklabs.watchtower.enable=false"
    restart: always

利用 Lifecycle Hook 在更新前自动备份

Watchtower 支持在更新容器前后触发自定义生命周期脚本(Lifecycle Hooks)。

通过在目标业务容器中定义标签,可以在 Watchtower 停掉容器拉取新镜像之前,自动执行一次数据库备份;在启动之后,执行数据库结构迁移(Migration):

services:
  my-app:
    image: my-app:latest
    container_name: my-app
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
      # 前置钩子:在停止旧容器前,自动在容器内执行导出备份
      - "com.centurylinklabs.watchtower.lifecycle.pre-update=/bin/sh -c 'pg_dump -U postgres dbname > /backups/pre_update.sql'"
      # 前置超时时间:给备份命令预留 30 秒执行时间
      - "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout=30s"
      # 后置钩子:在新容器启动就绪后,执行检查或初始化
      - "com.centurylinklabs.watchtower.lifecycle.post-update=/bin/sh -c 'python manage.py migrate'"

这一招彻底消除了自动化升级带来的“数据未备份便被冲掉”的后顾之忧!


开启 HTTP API 联动 CI/CD 触发更新

很多时候我们不希望死等每天凌晨 4 点的定时轮询。当我们在 GitHub Actions 编译出新的 Docker 镜像推送到仓库后,希望能立即通知服务器更新该容器!

Watchtower 内置了高性能 HTTP API 接口。

1. 远程一条 curl 命令即时触发更新

在你的 CI/CD 构建流水线或本地终端直接执行:

# 语法:curl -H "Authorization: Bearer <TOKEN>" http://<IP>:<PORT>/v1/update
curl -H "Authorization: Bearer SecretWatchtowerToken" \
  http://127.0.0.1:8088/v1/update

Watchtower 收到请求后会在 1 秒内立即唤醒并执行一次定向扫描升级,完工后自动回复 JSON 报告,优雅至极!


挂载私有镜像仓库拉取凭证

若你的业务使用的是 GitHub Container Registry(ghcr.io)或阿里云私有镜像,Watchtower 默认没有权限拉取。

优雅方案:挂载宿主机 Docker 认证凭证

宿主机只需在终端执行一次标准登录:

docker login ghcr.io -u YOUR_GITHUB_USER -p YOUR_GITHUB_PAT

此时宿主机在 /root/.docker/config.json 中已保存了鉴权凭据。只需在 Watchtower 编排中增加一行挂载:

    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /root/.docker/config.json:/config.json:ro

Watchtower 将自动继承宿主机的私有镜像权限,私有镜像照样丝滑全自动拉新!


接入 Telegram 镜像变动推送通知

在 /opt/watchtower/docker-compose.yml 中追加通知环境变量:

      # ================= 消息通知配置 =================
      - WATCHTOWER_NOTIFICATIONS=shoutrrr
      # Shoutrrr 标准 Telegram 链接格式
      - WATCHTOWER_NOTIFICATION_URL=telegram://123456:ABC-DEF@telegram?chats=1658239957
      # 核心:仅在检测到更新动作时才发送推送,无变动时保持静默
      - WATCHTOWER_NOTIFICATION_REPORT=true

重载生效:docker compose up -d。当任何一个容器完成升级后,手机 Telegram 立刻收到包含旧哈希、新哈希与容器名称的精炼汇报!


Dry-Run 空跑测试与废旧镜像清理验证

1. 现场 Dry-Run 演练(空跑核验,只看不动)

在不确定标签配置是否正确时,先执行一次带 --dry-run 的空跑指令:

服务器端:

docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  containrrr/watchtower:latest \
  --run-once \
  --dry-run \
  --label-enable

核验标准:输出明确列出哪些白名单容器发现了新镜像,且数据库等关键容器均被标注为 Skipping 跳过。

2. 核验废旧镜像清理彻底度

在执行完更新后,检查宿主机是否残存悬空无标签镜像:

服务器端:

docker images -f "dangling=true" -q

核验标准:输出为空,证明 --cleanup 机制完美奏效,磁盘干干净净!


常见报错与排障

Q1:新容器更新后报错闪退,旧容器已被删,如何秒级回滚?

  • 自愈方案:
    1. 查看故障容器日志定位根因:docker logs -n 50 FAILED_CONTAINER;
    2. 如果是新版镜像配置废弃,直接编辑该业务的 docker-compose.yml,将镜像 Tag 从 :latest 改回上一个稳定的具体版本号(例如 image: myapp:1.24.2);
    3. 执行 docker compose up -d,系统立刻重新拉取稳定版本满血复活。

Q2:为什么配置了白名单标签,Watchtower 依然提示没有找到更新?

  • 排查要点:
    1. 检查该容器的镜像 Tag 是否写死了不可变的具体版本号(如 nginx:1.24.0);
    2. 只有当同一个 Tag 在远程仓库的 SHA256 哈希发生变更时,Watchtower 才会识别出更新;若需自动跟新,Tag 必须使用 :latest 或主版本 Tag(如 :1、:alpine)。

Q3:日志中频繁出现 429 Too Many Requests?

  • 根因:Docker Hub 对未登录匿名 IP 限制每 6 小时仅 100 次请求。默认高频轮询会迅速烧光配额。
  • 自愈方案:在编排中配置 WATCHTOWER_SCHEDULE=0 0 4 * * * 降低频率为每天一次,并在宿主机执行 docker login 登录免费账号提升拉取限额。