一、 课前预习:核心基础概念白话解析
在深入 Watchtower 之前,我们需要先对几个 Docker 的底层概念建立直观的认识。如果你熟悉 docker run,这些概念你其实已经用过,只是没看到它们的“内在”。
- Docker 守护进程 (Docker Daemon) 与 Docker Engine API:
- 白话比喻:你在终端敲打的
docker ps等命令只是“服务员”(客户端),真正的“大厨”是后台默默运行的 Docker Daemon。服务员和大厨之间交流的菜单,就是 Docker Engine API。 - 技术释义:Daemon 负责管理容器、镜像、网络和存储。Watchtower 作为一个运行中的容器,之所以能控制其他容器,就是因为它通过某种方式(挂载
docker.sock)直接和 Daemon 这个“大厨”对话,调用了它的 API。
- 白话比喻:你在终端敲打的
- 镜像标签 (Tags) 与 摘要 (Digests):
- 白话比喻:Tag 就像书名(比如
nginx:latest),但书的内容可能会再版(镜像被作者更新)。Digest 就像书的 ISBN 码(一长串哈希值,如sha256:abc123...),只要内容变了,ISBN 码绝对不一样。 - 技术释义:Watchtower 判断镜像是否更新,不看 Tag(因为一直都是
latest),而是比对本地镜像库和远程仓库中的 Digest 是否一致。
- 白话比喻:Tag 就像书名(比如
- 容器生命周期 (Container Lifecycle):
- 概念:容器的生老病死:创建 (Created) -> 运行 (Running) -> 停止 (Stopped) -> 删除 (Removed)。Watchtower 的更新本质上就是走完旧容器的后半生,并开启新容器的前半生。
- 卷 (Volumes) 与 端口映射 (Ports):
- 白话比喻:卷是容器的“外置移动硬盘”,哪怕容器被删除了,数据还在硬盘里。端口映射是连接外部世界和容器的“专线电话”。
- 技术释义:为了保证服务连续性,Watchtower 在创建新容器时,会完全继承旧容器的“移动硬盘”和“专线电话”配置。
二、 核心机制:Watchtower 的工作流程
Watchtower 是如何发现并替换容器的?它的完整工作流可以分为以下五个步骤(默认每 24 小时或按 cron 表达式轮询一次):
1. 发现更新 (Discovery)
Watchtower 会定期向你配置的镜像仓库(如 Docker Hub、阿里云镜像库)发送请求,查询当前正在运行的容器镜像的最新 Digest(哈希值)。
- 判断逻辑:它会将远程仓库返回的最新 Digest 与本地宿主机上该镜像的 Digest 进行对比。如果两者不一致,说明作者发布了新版本,触发更新流程。
2. 拉取新镜像 (Pull)
一旦发现有新版本,Watchtower 会通知 Docker Daemon 将最新的镜像层下载到本地。
- 后台操作:等同于你在终端执行
docker pull <image_name>:<tag>。
3. 优雅停止旧容器 (Graceful Stop)
为了防止数据丢失或任务中断,Watchtower 不会粗暴地拔电源。
- 后台操作:它首先向旧容器发送
SIGTERM信号(请求容器自己收拾行李准备退出)。如果容器在默认的 10 秒内没有退出,它才会发送SIGKILL信号(强制拔电源)。这等同于docker stop <container_name>。
4. 完美复刻并启动新容器 (Recreate & Start)
这是 Watchtower 最核心的魔法! 它不仅要启动新容器,还要保证新容器和旧容器一模一样(除了镜像更新了)。
- 后台操作:Watchtower 会读取旧容器的所有启动参数——包括环境变量 (ENV)、卷挂载 (Volumes)、网络配置 (Networks)、重启策略 (Restart Policies)、暴露端口等。然后,它用这些一模一样的参数,加上刚刚拉取下来的新镜像,创建一个全新的容器并启动。
- 服务连续性:因为新的容器挂载了相同的 Volume,数据库的存量数据依然存在;因为绑定了相同的端口,用户的访问依然顺畅。中断时间仅仅是容器重启的那几秒钟。
5. 清理战场 (Cleanup)
默认情况下,如果启动成功,Watchtower 会把停止的旧容器以及悬空状态(Dangling)的旧镜像删除,防止服务器磁盘被撑爆。
三、 底层原理剖析:技术与 API 视角
作为初学者,你可能会好奇:Watchtower 本身也是个容器,它怎么有权限“杀掉”并“创建”其他容器?
核心秘密在于这行启动参数:-v /var/run/docker.sock:/var/run/docker.sock
/var/run/docker.sock是 Docker Daemon 监听的 Unix Socket(一种让同一台机器上的程序互相通信的管道)。- Watchtower 将宿主机的这个文件挂载到自己内部,相当于它拿到了“皇帝的兵符”。它可以通过 HTTP 请求直接向 Docker Daemon 发送指令。
以下是 Watchtower 行为与 Docker底层 API 的对应关系对比表:
| Watchtower 阶段 | 对应的基础 Docker CLI 命令 | 底层调用的 Docker Engine API (可靠来源:Docker 官方文档) | |||
|---|---|---|---|---|---|
| 获取旧容器配置 | docker inspect <container> |
GET /containers/{id}/json(获取环境变量、挂载、网络等详情) |
|||
| 检查/拉取新镜像 | docker pull <image> |
POST /images/create?fromImage=<image>(联系 Registry 下载新层) |
|||
| 停止旧容器 | docker stop <container> |
POST /containers/{id}/stop(发送 SIGTERM) |
|||
| 重命名/删除旧容器 | docker rm <container> |
DELETE /containers/{id}(释放旧容器占用的名称) |
|||
| 创建新容器 | docker create ... |
POST /containers/create(将第一步获取的 JSON 传参给该接口) |
|||
| 启动新容器 | docker start <container> |
POST /containers/{id}/start(让新容器跑起来) |
可靠来源参考:
- Docker Engine API 官方文档: (https://docs.docker.com/engine/api/) 定义了上述所有 HTTP 接口的交互标准。
- Watchtower GitHub 源码 (containrrr/watchtower): 在其源码的
pkg/container/client.go中,可以清晰看到它使用了 Docker 官方提供的 Go SDK (github.com/docker/docker/client) 来实例化这些 API 调用,完美印证了上述逻辑。
内部文件系统的变化:
在 Docker 的底层存储驱动(如 Overlay2)中,容器其实是一层只读的“镜像层”加上一层可写的“容器层”。更新过程中:
- 旧的读写层(旧容器运行产生的临时垃圾)被丢弃。
- 本地持久化卷(Volumes)中的数据毫发无损。
- 新容器会在新下载的只读镜像层之上,建立一个全新的读写层开始工作。
四、 总结与最佳实践
Watchtower 通过挂载 docker.sock 获取了与 Docker Daemon 通信的最高权限。它通过比对镜像 Digest 发现更新,再通过调用 Docker Engine API 完美复制旧容器的启动参数,实现了无缝“热替换”。
给初学者的避坑提示:
对于个人博客、小工具,全面自动更新非常爽。但在企业生产环境中,通常不会让 Watchtower 自动更新数据库(如 MySQL、PostgreSQL),因为数据库的大版本更新往往需要手动迁移数据结构。