Docker更新策略:VPS上的零停机容器更新
四种递进式方法更新VPS上的Docker容器,从简单的拉取替换到使用Traefik的零停机蓝绿部署。涵盖镜像固定、回滚操作、Diun通知和docker-rollout。
在VPS上更新Docker容器不一定意味着停机。正确的方法取决于你运行的服务以及能容忍多长时间的中断。个人博客可以承受docker compose up -d期间几秒钟的停机。面向付费客户的SaaS产品则不行。
本指南涵盖四种方法,从最简单到最可靠。每种方法建立在前一种的基础上。从适合你当前情况的方法开始,需要时再升级到下一个级别。
**前提条件:**一台运行Debian 12或Ubuntu 24.04的VPS,已安装Docker Engine 27+和Docker Compose v2。所有命令使用docker compose插件语法(不是已弃用的docker-compose v1二进制文件)。VPS上的Docker生产环境:会出什么问题以及如何解决
如何将Docker镜像固定到特定版本?
在compose文件中将镜像固定到特定的minor或patch版本。latest标签是一个移动目标,可能在没有警告的情况下引入破坏性更改。固定版本让你控制更新时机,并通过在本地保留旧镜像来实现回滚。
不同的标签策略有不同的风险:
| 标签格式 | 示例 | 风险等级 | 更新行为 |
|---|---|---|---|
latest |
nginx:latest |
高 | 任何版本,任何时间。无法知道什么改变了。 |
| 仅major | nginx:1 |
中高 | 可能从1.25跳到1.27。minor版本可能改变行为。 |
| minor | nginx:1.27 |
低 | 接收patch更新(1.27.0到1.27.3)。对大多数工作负载安全。 |
| patch | nginx:1.27.3 |
极低 | 精确版本。没有意外更新。手动更新。 |
| digest | nginx:1.27.3@sha256:6f12... |
最低 | 每次都是字节级相同的镜像。不受标签变更影响。 |
对于大多数生产服务,固定到minor版本(image: postgres:16.6)。这在安全补丁和稳定性之间取得了平衡。对于需要可重现性的服务(CI、受监管环境),固定到完整的digest。
services:
app:
image: myapp:2.4.1
# Not: image: myapp:latest
db:
image: postgres:16.6
更新前记录当前镜像的digest。回滚时会用到:
docker image inspect --format='{{index .RepoDigests 0}}' $(docker compose images app -q)
myapp@sha256:a1b2c3d4e5f6...
如何在Docker Compose中设置健康检查?
健康检查告诉Docker你的容器是否真正在工作,而不仅仅是在运行。所有零停机更新模式都依赖它。没有健康检查,Docker无法在移除旧容器之前知道新容器是否就绪。
在compose文件的每个服务中添加healthcheck块。test命令在容器内按指定间隔运行。只有在测试通过后,Docker才会将容器标记为healthy。
services:
app:
image: myapp:2.4.1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
各字段含义:
- test:要执行的命令。
CMD直接执行。如果需要shell功能如管道,使用CMD-SHELL。 - interval:检查间隔。15秒对web服务来说比较合理。
- timeout:等待命令完成的时间,超时则视为失败。
- retries:Docker将容器标记为
unhealthy之前需要的连续失败次数。 - start_period:容器启动后的宽限期。此窗口内的健康检查失败不计入失败阈值。设置足够长的时间让应用完成启动。
对于没有安装curl的服务,使用服务自带的检查工具:
db:
image: postgres:16.6
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
cache:
image: redis:7.4
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
启动服务后,检查健康检查是否通过:
docker compose ps
NAME IMAGE STATUS PORTS
app myapp:2.4.1 Up 2 minutes (healthy) 0.0.0.0:8080->8080/tcp
db postgres:16.6 Up 2 minutes (healthy) 5432/tcp
(healthy)状态表示你的健康检查已配置并且正在通过。如果看到(health: starting),说明容器仍在start_period内。如果看到(unhealthy),查看健康检查日志:
docker inspect --format='{{json .State.Health}}' $(docker compose ps -q app) | python3 -m json.tool
如何在VPS上更新Docker容器?
运行docker compose pull获取新镜像,然后运行docker compose up -d替换容器。Docker Compose会停止旧容器、移除它,并从更新后的镜像启动新容器。在新容器启动并通过健康检查期间,会有短暂中断(通常2-10秒)。
分步操作:简单更新
更新前先备份你的volumes。更新失败导致数据损坏比几秒钟的停机严重得多。
阅读新版本的changelog。检查是否有破坏性更改、已弃用的配置选项和必要的迁移步骤。这只需要五分钟,却能省下数小时的调试时间。
# Pull the new image
docker compose pull app
# Check what changed
docker compose up -d --dry-run
--dry-run标志(Docker Compose v2.20+)会显示Compose将要执行的操作,但不会实际执行。你会看到哪些容器将被重新创建:
DRY RUN MODE - service "app" - Pull
DRY RUN MODE - Container app-1 - Recreate
DRY RUN MODE - Container app-1 - Started
应用更新:
docker compose up -d app
[+] Running 1/1
✔ Container app-1 Started 0.8s
检查新容器是否healthy:
docker compose ps app
NAME IMAGE STATUS PORTS
app myapp:2.5.0 Up 15 seconds (healthy) 0.0.0.0:8080->8080/tcp
然后从服务器外部测试,确认服务可访问:
curl -s -o /dev/null -w "%{http_code}" https://app.example.com/health
200
何时更新Docker容器?
不同类型的更新紧急程度不同。统一的更新计划要么带来不必要的风险,要么导致错过安全补丁。
- **安全补丁(CVE):**立即应用。订阅你使用的镜像的安全公告。公开暴露的容器中已知CVE在披露后数小时内就会被利用,不是数天。
- patch版本(如2.4.1到2.4.2):每周或每两周安排。这些是bug修复。阅读changelog,更新,验证。
- minor版本(如2.4到2.5):每月安排。如果有staging环境,先在那里测试。检查changelog中的行为变更。
- major版本(如2.x到3.x):规划并测试。major版本会破坏兼容性。阅读迁移指南。在单独的VPS或本地环境中测试后再动生产环境。
如何将Docker容器回滚到之前的镜像?
Docker Compose没有内置的回滚命令。要回退:编辑compose文件,将镜像固定到之前的标签或digest,然后运行docker compose up -d。容器会使用旧镜像重启。前提是你在本地保留了旧镜像(更新后不要立即运行docker image prune)。
分步回滚
假设你将myapp从2.4.1更新到2.5.0,而新版本有问题。
- 检查旧镜像是否仍在本地可用:
docker images myapp
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp 2.5.0 abc123def456 2 hours ago 185MB
myapp 2.4.1 789fed654cba 2 weeks ago 182MB
- 编辑compose文件,固定到之前的版本:
services:
app:
image: myapp:2.4.1
- 执行回滚:
docker compose up -d app
[+] Running 1/1
✔ Container app-1 Started 0.7s
- 检查回滚是否成功:
docker compose ps app
NAME IMAGE STATUS PORTS
app myapp:2.4.1 Up 10 seconds (healthy) 0.0.0.0:8080->8080/tcp
如果旧镜像已被清理,Docker会从注册中心重新下载(前提是标签仍然存在)。为了最大安全性,在更新前记下完整的digest(sha256:...)。digest是不可变的,而标签可以被覆盖。
用预更新脚本实现自动化
每次更新前保存当前状态,这样回滚始终只需一条命令:
#!/bin/bash
# save-state.sh - Run before every update
COMPOSE_FILE="${1:-docker-compose.yml}"
DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="./rollback/${DATE}"
mkdir -p "${BACKUP_DIR}"
cp "${COMPOSE_FILE}" "${BACKUP_DIR}/"
docker compose ps --format json > "${BACKUP_DIR}/containers.json"
docker compose images --format json > "${BACKUP_DIR}/images.json"
echo "State saved to ${BACKUP_DIR}"
chmod 700 save-state.sh
Watchtower在2026年还在维护吗?
Watchtower已于2025年12月17日归档。维护者不再使用Docker,已停止开发。最后一个版本是v1.7.1。更重要的是,Watchtower的Docker SDK使用API v1.25,但Docker Engine 29已将最低API版本提升到v1.44。除非你在daemon配置中手动降低API最低版本(DOCKER_MIN_API_VERSION=1.25),否则Watchtower与当前Docker版本不兼容。那是权宜之计,不是解决方案。
如果你现在还在用Watchtower,请规划迁移。需要自动化更新通知而不自动重启,用Diun。需要自动化零停机更新,用docker-rollout配合反向代理。
Diun如何通知你Docker镜像更新?
Diun(Docker Image Update Notifier)监控你的Docker注册中心,在有新镜像版本可用时发送通知。它不更新容器。它告诉你有更新存在,让你可以阅读changelog并按自己的节奏更新。这是"先了解再行动"的方式。
将Diun添加到现有的compose文件或创建一个专用的:
services:
diun:
image: crazymax/diun:4
command: serve
volumes:
- "diun-data:/data"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
environment:
TZ: "Europe/Berlin"
DIUN_WATCH_WORKERS: "10"
DIUN_WATCH_SCHEDULE: "0 6 * * *"
DIUN_PROVIDERS_DOCKER: "true"
DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: "true"
DIUN_NOTIF_SLACK_WEBHOOKURL_FILE: "/run/secrets/slack_webhook"
secrets:
- slack_webhook
restart: unless-stopped
secrets:
slack_webhook:
file: ./secrets/slack_webhook.txt
volumes:
diun-data:
Slack webhook URL放在secrets文件中,而不是环境变量中,因为Docker secrets会将其从docker inspect输出和进程列表中隐藏。创建权限受限的secrets文件:
mkdir -p secrets
echo "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" > secrets/slack_webhook.txt
chmod 600 secrets/slack_webhook.txt
关键配置说明:
- DIUN_WATCH_SCHEDULE:Cron表达式。
0 6 * * *每天06:00检查。根据你的维护窗口调整。 - DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT:设为
true时,Diun监控所有运行中的容器。设为false并使用labels进行选择性监控。 - Docker socket挂载:只读(
:ro),因为Diun只读取容器元数据。它不会启动或停止任何容器。
对于选择性监控(建议用于较大的stack),将WATCHBYDEFAULT设为false,并在要监控的容器上添加labels:
services:
app:
image: myapp:2.4.1
labels:
- "diun.enable=true"
- "diun.watch_repo=true"
启动Diun并查看日志:
docker compose up -d diun
docker compose logs diun --tail 20
diun | Thu, 19 Mar 2026 06:00:01 CET INF Starting Diun version=v4.31.0
diun | Thu, 19 Mar 2026 06:00:01 CET INF Configuration loaded from 5 environment variable(s)
diun | Thu, 19 Mar 2026 06:00:02 CET INF Cron triggered
diun | Thu, 19 Mar 2026 06:00:03 CET INF New image found image=docker.io/myapp:2.5.0 provider=docker
当Diun发现新镜像时,会发送Slack消息,包含镜像名称、当前标签和新标签。由你决定是否以及何时更新。
docker-rollout如何实现零停机更新?
docker-rollout是一个Docker CLI插件,为Compose服务执行蓝绿部署。它从更新后的镜像启动新容器,等待健康检查通过,然后移除旧容器。流量永远不会到达不健康的容器,因为反向代理只将流量路由到healthy的容器。
要求:
- 一个反向代理(Traefik、Caddy或nginx-proxy)将流量路由到你的服务
- compose文件中定义了健康检查
- 服务上没有
container_name指令(docker-rollout管理容器名称) - 服务上没有直接的
ports映射(反向代理处理端口暴露)
安装docker-rollout
mkdir -p /usr/local/lib/docker/cli-plugins
curl -fsSL https://raw.githubusercontent.com/wowu/docker-rollout/main/docker-rollout \
-o /usr/local/lib/docker/cli-plugins/docker-rollout
chmod +x /usr/local/lib/docker/cli-plugins/docker-rollout
安装后应显示版本号:
docker rollout --version
docker-rollout version v0.13
示例:Traefik + docker-rollout
一个最小的compose文件,用于在Traefik后面运行带健康检查的web应用。应用没有ports或container_name,因为docker-rollout需要管理扩缩容。
services:
traefik:
image: traefik:3.3
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
restart: unless-stopped
app:
image: myapp:2.4.1
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=web"
- "traefik.http.services.app.loadbalancer.server.port=8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
restart: unless-stopped
零停机部署
拉取新镜像,然后使用docker rollout代替docker compose up -d:
docker compose pull app
docker rollout app
==> Scaling 'app' to '2' instances
Container myproject-app-2 Creating
Container myproject-app-2 Created
Container myproject-app-2 Starting
Container myproject-app-2 Started
==> Waiting for new containers to be healthy (timeout: 60 seconds)
==> Stopping and removing old containers
在此过程中,Traefik通过Docker socket检测到新容器,在其变为healthy后将流量路由到它,并在移除旧容器之前停止向其路由。你的用户不会感到任何中断。
如果新容器未通过健康检查,docker-rollout会中止操作,旧容器继续运行。无需手动干预。
什么是Docker和Traefik的蓝绿部署?
蓝绿部署运行你服务的两个副本(蓝色和绿色)。一个服务实时流量,另一个保持空闲。部署时,你更新空闲副本,验证其正常工作,然后切换流量。通过切回上一个副本,可以实现即时回滚。
这就是docker-rollout背后的概念,但你可以手动实现以获得更多控制。使用Traefik动态配置的最小示例:
services:
traefik:
image: traefik:3.3
command:
- "--providers.file.directory=/etc/traefik/dynamic"
- "--providers.file.watch=true"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
volumes:
- "./traefik/dynamic:/etc/traefik/dynamic:ro"
restart: unless-stopped
app-blue:
image: myapp:2.4.1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
app-green:
image: myapp:2.4.1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
Traefik动态配置文件控制哪个副本接收流量:
# traefik/dynamic/app.yml
http:
routers:
app:
rule: "Host(`app.example.com`)"
service: app
entryPoints:
- web
services:
app:
loadBalancer:
servers:
- url: "http://app-blue:8080"
部署步骤:更新app-green的镜像,启动它,等待其变为healthy,然后编辑app.yml指向app-green。因为设置了watch=true,Traefik会自动应用更改。要回滚,编辑文件重新指向app-blue。
这种方法比docker-rollout需要更多工作。当你需要对切换有明确控制、想在切换流量前对新版本运行冒烟测试、或多个服务需要同时切换时使用。
应该使用哪种更新方法?
选择与你的停机容忍度和基础设施复杂度匹配的方法。
| 方法 | 停机时间 | 复杂度 | 回滚 | 需要反向代理 | 适用场景 |
|---|---|---|---|---|---|
docker compose pull + up |
2-10秒 | 低 | 手动(编辑compose文件) | 否 | 个人项目、内部工具 |
| Diun + 手动更新 | 同上 | 低 | 同上 | 否 | 希望更新前有可见性的团队 |
| docker-rollout | 无 | 中 | 自动(失败时中止) | 是 | 单台VPS上的生产服务 |
| 蓝绿(手动) | 无 | 高 | 即时(切换配置文件) | 是 | 多服务stack、受监管环境 |
决策流程:
- 2-10秒的停机可以接受?使用
docker compose pull && docker compose up -d。 - 想在应用更新前了解更新内容?添加Diun。
- 需要零停机?有反向代理?使用docker-rollout。
- 需要对流量切换有明确控制?手动实现蓝绿部署。
出了问题?
容器启动但显示(unhealthy)
检查健康检查命令。在容器内手动执行:
docker compose exec app curl -f http://localhost:8080/health
如果失败,问题出在你的应用,不是Docker。查看应用日志:
docker compose logs app --tail 50
旧镜像已被清理,无法回滚
如果标签在注册中心仍然存在,docker compose pull会重新下载。如果你通过digest固定,Docker会下载精确的镜像,不受标签变更影响:
image: myapp:2.4.1@sha256:789fed654cba...
docker-rollout在部署时卡住
健康检查在超时时间内未通过。检查健康检查的间隔和重试次数。增加超时时间:
docker rollout -t 120 app
Docker更新后Watchtower停止工作
Docker Engine 29要求最低API v1.44。Watchtower使用API v1.25。迁移到Diun获取通知,或使用docker-rollout实现自动化零停机更新。
Diun未检测到新镜像
检查DIUN_WATCH_SCHEDULE中的cron计划。手动触发扫描:
docker compose exec diun diun image list
检查Diun日志中的注册中心认证错误:
docker compose logs diun --tail 30
版权所有 2026 Virtua.Cloud。保留所有权利。 本内容为 Virtua.Cloud 团队原创作品。 未经书面许可,禁止复制、转载或再分发。