把服务容器化之后,更新镜像往往成了机械劳动:定期 docker compose pull 然后 up -d。如果挂在机器上的容器多了,很容易懒得挨个去敲命令更新。
Watchtower 就是专门用来自动化这个过程的哨兵容器:它定期拉取远端镜像仓库,比对 digest 哈希,发现新版就自动拉取并按原有参数平滑重建容器。
但这玩意如果无脑开全局更新,非常容易翻车:比如数据库大版本升级导致数据结构不兼容直接挂掉,或者 upstream 镜像打了个 broken tag 把生产服务搞崩。
这篇笔记记录我生产环境的 Watchtower 跑法:只给白名单容器自动更新(label 过滤)、更新前执行备份 Hook、推送 Telegram 告警,以及配置私有镜像凭证。
全自动更新的隐患与防翻车原则
在正式把 Watchtower 投入生产之前,请必须将以下三条铁律刻入脑海:
┌────────────────────────────────────────────────────────────────────────┐
│ Watchtower 生产级分级管控哲学 │
├───────────────────────────────────┬────────────────────────────────────┤
│ 🟢 允许自动跟更 (白名单放行) │ 🔴 严禁自动跟更 (坚决物理隔离) │
│ • Nginx / Caddy 反向代理网关 │ • MySQL / PostgreSQL 生产数据库 │
│ • 静态博客前端 / 个人小工具 │ • Vaultwarden 私有密码保险箱 │
│ • 探针 Agent (Komari / 哪吒) │ • 涉及复杂数据迁移的核心存储 │
│ • 无状态爬虫与聚合搜索 API │ • 固定商业特定生产版本的核心服务 │
└───────────────────────────────────┴────────────────────────────────────┘
- 绝对禁止全盘无差别更新(Label Enable 是生命线):严禁自动更新有状态数据库!数据库跨大版本升级(例如 PostgreSQL 15 升至 16)会导致数据目录格式不兼容,容器重启直接死锁闪退;
- 拒绝旧镜像占满磁盘(Auto Cleanup):拉取新镜像后,旧镜像如果不自动清理,会变成
<none>:<none>孤儿悬空镜像。几周时间就能吞掉几十 GB 物理硬盘,必须开启自动抹除; - 滚动更新平滑切换(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登录免费账号提升拉取限额。
还没有人在这篇手记留下纸条,来做第一个亮灯的人吧。