基于 Docker Compose 的蓝绿零停机部署实践
适用场景:单机 Tomcat 应用(
backend),固定宿主端口 8080,要求更新发布期间服务不中断。
方案:nginx(8080 入口) + 双实例(blue / green)蓝绿切换,发布时"先启新 → 探活 → 原子切换 → 删旧"。
1. 背景与目标
原问题:旧部署脚本是"先停旧、再启新"(docker stop → docker rm → docker run),存在两个缺陷:
rm之后、run的新容器就绪之前,8080 端口无服务监听,更新期间服务必然中断;- 无健康检查、无失败回滚,新镜像起不来时服务会长时间不可用。
本方案目标:
- 发布期间零停机(切换为 nginx reload 原子操作,毫秒级);
- 新版本探活失败自动不切换,旧版本随时可回滚;
- 用 docker compose 声明式管理,替代手写
docker run长参数。
架构:
宿主 80
│
┌────▼────┐
│ nginx │ ← 挂载 /data/applet/nginx/conf.d
└────┬────┘
┌──────────┴──────────┐
(upstream 指向其中一个)
▼ ▼
┌───────────────┐ ┌───────────────┐
│ backend-blue │ │ backend-green │
│ 当前线上版本 │ │ 发布目标版本 │
└───────────────┘ └───────────────┘
docker 网络 appnet(容器间按容器名 DNS 互访)
2. 命名约定
| 项 | 值 | 说明 |
|---|---|---|
| 项目/镜像前缀 | backend | 镜像 tag 形如 backend:blue / backend:green |
| 服务名(compose) | backend-blue / backend-green | |
| 容器名(container_name) | backend-blue / backend-green | 与 nginx upstream 对齐,保证 DNS 可解析 |
| nginx 容器 | applet-nginx | |
| 网络 | appnet(external) | 需手动创建一次 |
| 数据目录 | /data/applet/backend/conf | 只读配置,blue/green 共享 |
| 日志目录 | /data/applet/backend/logs-blue / logs-green | 双实例分开,避免并发写 |
如你的实际镜像 tag 是
applet:blue之类,全文替换对应名称即可,结构不变。
3. 前置条件
- Docker 20.10+,且已安装 Compose v2 插件(检查:
docker compose version)。 - 服务器磁盘有足够空间存放新旧两个镜像(
docker system df可查)。 - 8080 端口当前无其他进程/容器占用(若有旧容器先清理,见 4.2)。
4. 一次性准备(首次搭建)
4.1 安装 Compose v2 插件(如未安装)
docker compose version # 先检查;若报 'compose' is not a docker command 则继续
Debian / Ubuntu:
apt-get update && apt-get install -y docker-compose-plugin
RHEL / CentOS(docker-ce 源):
yum install -y docker-compose-plugin
离线/内网服务器(手动装插件,通用):
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p "$DOCKER_CONFIG/cli-plugins"
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 \
-o "$DOCKER_CONFIG/cli-plugins/docker-compose"
chmod +x "$DOCKER_CONFIG/cli-plugins/docker-compose"
docker compose version # 输出 v2.x 即成功
4.2 清理旧容器、创建网络
# 若之前用 docker run 起过同名/占 8080 的容器,先停删
docker rm -f backend applet 2>/dev/null || true
docker ps | grep 8080 # 确认 8080 已释放
# 创建 compose 使用的 external 网络(只需一次)
docker network create appnet
4.3 准备 nginx 配置
创建 /data/applet/nginx/conf.d/app.conf(初始指向 blue,即当前线上版本):
upstream app {
server backend-blue:8080;
}
server {
listen 8080;
location / {
proxy_pass http://app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
⚠️ 关键点:nginx 启动时会解析 upstream 主机名,upstream 指向的容器必须已存在,否则 nginx 报
host not found in upstream直接启动失败。所以顺序永远是"先起后端,再起 nginx"。
4.4 编写 compose 文件
在部署目录(如 /opt/docker-compose/)新建 compose.yaml(⚠️ 文件名必须是 compose.yaml / compose.yml / docker-compose.yaml / docker-compose.yml 之一,否则报 “no configuration file provided”):
services:
nginx:
image: nginx:1.30.4 # 先 docker pull nginx:1.30.4 验证 tag 存在
container_name: applet-nginx
ports:
- "8080:8080"
volumes:
- /data/applet/nginx/conf.d:/etc/nginx/conf.d:ro
networks: [appnet]
restart: unless-stopped
backend-blue:
image: backend:blue # 当前线上版本
container_name: backend-blue
ulimits:
nofile:
soft: 65535
hard: 65535
volumes:
- /data/applet/backend/conf:/usr/local/tomcat/conf/config
- /data/applet/backend/logs-blue:/usr/local/tomcat/logs
networks: [appnet]
healthcheck:
test: ["CMD-SHELL", "curl -s -o /dev/null http://127.0.0.1:8080/ || exit 1"]
interval: 5s
timeout: 3s
retries: 3
start_period: 30s
restart: unless-stopped
backend-green:
image: backend:green # 发布目标版本
container_name: backend-green
ulimits:
nofile:
soft: 65535
hard: 65535
volumes:
- /data/applet/backend/conf:/usr/local/tomcat/conf/config
- /data/applet/backend/logs-green:/usr/local/tomcat/logs
networks: [appnet]
healthcheck:
test: ["CMD-SHELL", "curl -s -o /dev/null http://127.0.0.1:8080/ || exit 1"]
interval: 5s
timeout: 3s
retries: 3
start_period: 30s
restart: unless-stopped
networks:
appnet:
external: true
⚠️ 健康检查依赖容器内存在
curl。官方 tomcat 镜像默认没有 curl,healthcheck 会一直 unhealthy。解决:Dockerfile 里apt-get install -y curl(或换成镜像里确认存在的工具如 wget)。
5. 初始部署(首次上线)
在 compose.yaml 所在目录执行:
# 1. 先起后端(blue),再起 nginx —— 保证 upstream 主机名可解析
docker compose up -d backend-blue --no-deps
docker compose up -d nginx --no-deps
# 2. 确认全部 healthy
docker compose ps
# NAME STATUS
# applet-nginx running (healthy)
# backend-blue running (healthy)
# 3. 宿主机验证访问
curl http://127.0.0.1:8080/
若 blue 不 healthy,先排查(见第 9 节),不要继续往下走。
6. 常规发布流程(更新版本,零停机)
假设当前线上是 blue,发布新版本 v2:
# ① 加载新镜像并打成 green 标签(旧 blue 不受任何影响)
docker load -i backend-v2.tar
docker tag backend:v2 backend:green
# ② 只启动 green 实例(--no-deps:只操作目标服务)
docker compose up -d backend-green --no-deps
# ③ 等待 green 健康检查通过(成功 1 次即 healthy)
while [ "$(docker inspect --format '{{.State.Health.Status}}' backend-green)" != "healthy" ]; do
sleep 5
done
docker compose ps backend-green
# ④ 原子切换:把 nginx upstream 指向 green 并 reload(毫秒级,流量无缝切换)
sed -i 's/backend-blue/backend-green/' /data/applet/nginx/conf.d/app.conf
docker compose exec nginx nginx -s reload
# ⑤ 验证新版本
curl http://127.0.0.1:8080/ # 业务可用
docker compose exec nginx tail -5 /var/log/nginx/error.log # 无 upstream/502 报错
# ⑥ 确认无误后,下线旧版本
docker compose rm -sf backend-blue
发布完成后,green 变为当前线上,下次发布时角色对调(新镜像打成 blue,走同样的流程)。
7. 回滚流程
7.1 新版本还在(green 未删)——最简回滚
发现新版本有问题,只要把流量切回去即可,无需动容器:
sed -i 's/backend-green/backend-blue/' /data/applet/nginx/conf.d/app.conf
docker compose exec nginx nginx -s reload
# 需要的话再删掉有问题的 green
docker compose rm -sf backend-green
7.2 新版本已上线、旧容器已删——全量回滚
# 旧镜像还在本地(未覆盖 tag),重新起旧版本
docker compose up -d backend-blue --no-deps
# 等 healthy 后切回
while [ "$(docker inspect --format '{{.State.Health.Status}}' backend-blue)" != "healthy" ]; do sleep 5; done
sed -i 's/backend-green/backend-blue/' /data/applet/nginx/conf.d/app.conf
docker compose exec nginx nginx -s reload
# 删掉问题版本
docker compose rm -sf backend-green
注意:如果发布时把新镜像 tag 成了
backend:green而旧镜像就是 green(未换 tag),rollback 前需先docker tag恢复旧镜像。建议每次发布用新 tag(如 v2、v3),旧镜像自然保留,回滚零成本。
8. 验证清单(发布后逐项确认)
-
docker compose ps:nginx 与当前线上实例均为healthy -
curl http://127.0.0.1:8080/返回正常业务响应 - nginx error.log 无
no live upstreams/ 502 报错 - 业务日志(logs-green 或 logs-blue)正常输出,无启动异常
- 发布记录中标记了"当前线上 = green/blue"(建议在 compose 文件注释或发布脚本里维护)
9. 常见问题排查
| 现象 | 原因 | 处理 |
|---|---|---|
docker: 'compose' is not a docker command | 未装 Compose v2 插件 | 见 4.1 |
no configuration file provided: not found | 文件名不是 compose.yaml 等默认名 | 改名或加 -f 文件名 |
nginx 启动失败:host not found in upstream | upstream 指向的容器不存在 / 容器名与 compose 服务名不一致 | 先起后端再起 nginx;检查 container_name 与 upstream 一致 |
服务一直 unhealthy | 镜像内无 curl(ExitCode 127);或探测路径返回 404(-f 误判);或 start_period 不够 | docker inspect --format '{{json .State.Health}}' <容器名> 看 Log 输出;装 curl / 去掉 -f / 调大 start_period |
| 访问 502 Bad Gateway | upstream 指向的实例没起 / 切换后未 reload | docker compose ps 确认实例状态;重跑 reload |
| nginx 启动报端口占用 | 旧容器仍占 8080 | docker rm -f 旧容器后重试 |
10. 固化发布脚本(建议)
把第 6 节流程固化为 publish.sh,发布只需一个参数(新版本 tar 包路径),减少人工操作失误:
#!/bin/bash
# 用法:./publish.sh backend-v2.tar
set -euo pipefail
TAR="${1:?用法: $0 <新版本tar包>}"
TARGET=green # 每次对调:当前线上是 blue 就发 green,反之亦然
ACTIVE=$(grep -o 'server backend-[a-z]*:' /data/applet/nginx/conf.d/app.conf | head -1 | cut -d- -f3 | cut -d: -f1)
echo "[1/5] 加载并打标签 $TAR -> backend:$TARGET"
docker load -i "$TAR"
docker tag backend:v2 backend:$TARGET
echo "[2/5] 启动 $TARGET"
docker compose up -d backend-$TARGET --no-deps
echo "[3/5] 等待 $TARGET healthy"
while [ "$(docker inspect --format '{{.State.Health.Status}}' backend-$TARGET)" != "healthy" ]; do sleep 5; done
echo "[4/5] 切换 nginx -> $TARGET"
sed -i "s/backend-$ACTIVE/backend-$TARGET/" /data/applet/nginx/conf.d/app.conf
docker compose exec nginx nginx -s reload
echo "[5/5] 下线 $ACTIVE"
docker compose rm -sf backend-$ACTIVE
echo "完成:当前线上 = $TARGET"
注意:
docker tag的源 tag 按实际 tar 包内镜像 tag 调整(如backend:v2或直接backend:latest)。
11. 运维注意事项
- 日志分目录:blue/green 必须挂不同 logs 目录,否则双实例并存期 Tomcat 并发写
catalina.out会互相踩。 - conf 共享只读:
/data/applet/backend/conf被双实例共享,不要在运行中由应用改写,否则会影响两侧。 - 健康检查工具:镜像内必须预装 curl(或 wget),否则 healthcheck 形同虚设。
- 切换前一定等 healthy:容器起来了不代表 Tomcat 就绪,探活通过再 reload,避免瞬间 502。
- 旧镜像保留:发布用递增 tag(v1→v2→v3),回滚零成本;若用固定 latest 必须保留备份 tag。
- 防火墙:8080 端口需在安全组/iptables 放行(如原来已放行则无需处理)。
更多推荐

所有评论(0)