用 Docker 搭建蓝绿部署的完整流程
先把蓝绿部署说成人话
之前我一直觉得“蓝绿部署”听起来挺玄学,好像非得有 Kubernetes、服务网格、流量染色那一大套东西才能搞。结果真动手以后发现,在 Docker 这种单机或者小规模部署里,它其实就是两个版本的应用容器同时活着,一个接流量,一个空跑或者准备接流量。等新版稳定了,再把 Nginx 这类反向代理的 upstream 一换,流量就过去了。
所以这里说的 blue 和 green,本质上就是两个应用容器,比如 web-blue 和 web-green。它们可以跑同一个端口,比如内部都是 3000,但是不让用户直接访问容器端口,用户只访问 Nginx。Nginx 再决定把请求转给 blue 还是 green。
这套东西适合单体应用、容器化服务、几台服务器以内的小团队。要是你已经多机房、多节点、还要灰度百分比、链路追踪、自动扩缩容,那 Docker Compose 这套就不太够了,得往编排平台靠一靠。
目录结构先摆好
我自己的做法是一个部署目录单独放,别和项目源码混成一团。比如:
blue-green-demo/
├── app/
│ └── Dockerfile
├── nginx/
│ ├── server.conf
│ └── active_backend.conf
├── .deploy/
│ └── current_target
├── deploy.sh
└── docker-compose.yml
.deploy 是用来记录当前流量在哪个环境的。这个文件看起来不起眼,但是脚本里全靠它判断下一个要部署到 blue 还是 green。要是你手动部署,那也行,就是很容易哪天睡懵了,把 Nginx 切到一个还没健康检查过的容器上面。
一个能用的 Dockerfile
应用镜像这里随便给一个 Node 例子,你换成 Python、Go、Java 都行。重点是:镜像里最好有 /healthz 或者 /health 这样的接口,不然健康检查很烦。
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "src/server.js"]
如果你的项目没有健康检查接口,那就自己加一个。别嫌麻烦,后面 Docker Compose 健康检查、部署脚本等待容器 ready,都得靠这个。我一开始偷懒没加,结果脚本里一直判断不出来容器是否健康,后来才想起来这玩意儿不能少。
Docker Compose 让 blue 和 green 同时活着
docker-compose.yml 可以这样写:
services:
web-blue:
image: registry.example.com/blue-green-demo:${APP_VERSION:-latest}
container_name: blue-green-web-blue
expose:
- "3000"
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/healthz"]
interval: 10s
timeout: 5s
retries: 12
restart: unless-stopped
web-green:
image: registry.example.com/blue-green-demo:${APP_VERSION:-latest}
container_name: blue-green-web-green
expose:
- "3000"
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/healthz"]
interval: 10s
timeout: 5s
retries: 12
restart: unless-stopped
nginx:
image: nginx:1.27-alpine
container_name: blue-green-nginx
ports:
- "80:80"
volumes:
- ./nginx/server.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/active_backend.conf:/etc/nginx/conf.d/active_backend.conf:ro
depends_on:
web-blue:
condition: service_healthy
web-green:
condition: service_healthy
restart: unless-stopped
这里有个地方要注意,web-blue 和 web-green 都用了 expose: 3000,但是没有映射宿主机端口。这样做是为了不让用户绕过 Nginx 直接访问容器。要是你把它们都映射成 3001:3000、3002:3000,那本地测试挺方便,线上就别这么干了,迟早会有同事或者监控脚本直接打到旧端口上。
另外,nginx:alpine 里有没有 wget,取决于你用的镜像版本。我这里默认它可用。如果你的镜像里没有,可以换 curl,或者干脆在应用容器里用 node -e 做健康检查。命令写得难看一点没事,跑通最重要。
Nginx 才是真正切流量的开关
nginx/server.conf:
server {
listen 80;
location / {
proxy_pass http://active_backend;
proxy_http_version 1.1;
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;
}
location /healthz {
proxy_pass http://active_backend;
}
}
nginx/active_backend.conf 初始可以指向 blue:
upstream active_backend {
server web-blue:3000;
keepalive 32;
}
真正部署新版本的时候,不是重启应用容器来切流量,而是改这个 active_backend.conf,把 web-blue 换成 web-green,然后 reload Nginx。Nginx reload 是很轻的动作,不会断开监听,也不会像重启容器那样粗暴。
这里我踩过一个坑:active_backend.conf 这个文件如果不存在,Nginx 容器起不来。结果你以为应用挂了,其实只是配置文件没准备。所以脚本里最好先判断一下,或者第一次 docker compose up 之前手动创建。
部署脚本别靠手敲
下面是我自己的 deploy.sh,思路很简单:
- 读取当前流量在哪个环境。
- 新版本部署到另一个环境。
- 等新版本健康检查通过。
- 改 Nginx upstream。
- reload Nginx。
- 记录当前流量已经切到新版本。
#!/usr/bin/env bash
set -euo pipefail
mkdir -p .deploy
if [ ! -f .deploy/current_target ]; then
printf 'blue\n' > .deploy/current_target
fi
if [ ! -f nginx/active_backend.conf ]; then
printf 'upstream active_backend {\n server web-blue:3000;\n keepalive 32;\n}\n' > nginx/active_backend.conf
fi
current="$(tr -d '[:space:]' < .deploy/current_target)"
if [ "$current" = "blue" ]; then
new="green"
else
new="blue"
fi
image_tag="${1:-$(git rev-parse --short HEAD 2>/dev/null || echo latest)}"
echo "当前流量在 $current"
echo "准备部署到 $new,镜像 tag: $image_tag"
# 本地构建就改成 docker compose build,远程镜像就 pull
APP_VERSION="$image_tag" docker compose pull "web-$new" || true
APP_VERSION="$image_tag" docker compose up -d --no-deps "web-$new"
cid="$(docker compose ps -q "web-$new")"
until [ "$(docker inspect --format='{{.State.Health.Status}}' "$cid")" = "healthy" ]; do
echo "等待 web-$new 健康检查通过..."
sleep 2
done
# 切流量
printf 'upstream active_backend {\n server web-%s:3000;\n keepalive 32;\n}\n' "$new" > nginx/active_backend.conf
docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload
printf '%s\n' "$current" > .deploy/previous_target
printf '%s\n' "$new" > .deploy/current_target
echo "流量已切换到 $new,镜像 tag: $image_tag"
脚本里最关键的几行其实就是生成新的 active_backend.conf,然后 nginx -s reload。别小看这个,前面部署、健康检查、等待、镜像拉取,都是为了这几行命令别出事。
如果你用的是本地镜像,没有 registry,那把 docker compose pull 那段换成:
APP_VERSION="$image_tag" docker compose build "web-$new"
APP_VERSION="$image_tag" docker compose up -d --no-deps "web-$new"
不过本地 build 的话,blue 和 green 很容易指向同一个镜像 tag,测试切换不太直观。最好给每个版本打个 tag,比如 v20260912-abc123。
回滚脚本也别省
蓝绿部署最大的好处就是回滚快。发现新版本不行,只要把 Nginx 再切回旧环境就行了。旧环境别急着 stop,先留着观察几分钟。
rollback.sh 可以这样:
#!/usr/bin/env bash
set -euo pipefail
target="$(tr -d '[:space:]' < .deploy/previous_target)"
if [ -z "$target" ]; then
echo "没有 previous_target,不能回滚"
exit 1
fi
printf 'upstream active_backend {\n server web-%s:3000;\n keepalive 32;\n}\n' "$target" > nginx/active_backend.conf
docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload
printf '%s\n' "$target" > .deploy/current_target
echo "流量已回滚到 $target"
这个脚本只是切流量,不会立刻删除刚部署失败的新版本。我个人建议先别删,留着看看日志,等确认问题定位完了再 docker compose stop 或者 rm。
第一次跑起来的命令
假设你把应用镜像推到 registry 了,或者本地已经 build 出 latest,那先起来一组:
cd blue-green-demo
docker compose up -d
docker compose ps
curl -i http://localhost/healthz
如果你能看到 200,说明 blue 活着,Nginx 也在正常转发。然后部署新版本:
./deploy.sh v20260912-fix-login
切过去以后再看:
curl -i http://localhost/healthz
docker compose logs --tail 100 web-green
docker compose logs --tail 100 nginx
不出问题的话,基本就算部署成功了。
几个真会踩到的坑
第一,别把 blue 和 green 的端口直接暴露给用户。蓝绿部署的前提是流量入口只有一个。你要是同时开放多个应用端口,那切换就没什么意义了。
第二,健康检查别写得太松。我见过有人 healthz 只返回 {},但是数据库已经挂了,页面照样 200。这种健康检查等于没做。至少检查依赖,比如数据库、Redis、外部接口能不能通。
第三,切换 Nginx 后不要马上杀旧容器。最好留 5 到 30 分钟,具体时间看你业务。要是用户有长连接、WebSocket、大文件上传,切流量那一瞬间可能会有点影响。这个要提前知道,不要等用户反馈了才想起来。
第四,数据库迁移不能想当然。蓝绿部署只是解决应用容器切换,不会替你解决 schema 不兼容的问题。如果新版本要改表结构,最好做向后兼容:先加新字段,旧版本还能跑;切换完成以后,再清理旧字段。要是迁移脚本会把旧版本打挂,那就不叫回滚了,叫现场火化。
第五,镜像 tag 别滥用 latest。演示用一下没什么,线上真的会出事故。你这次部署 latest,下次还是 latest,回滚的时候发现两边镜像一模一样,那才尴尬。
这套流程适合什么情况
中小团队、单体 Docker 应用、一两台服务器、不需要特别复杂的发布策略,用这套就挺舒服。它比直接重启容器安全一点,也比一上来就搞平台简单一点。
我现在的习惯是:本地开发随便 restart,测试环境用 Docker Compose 起 blue/green,预发和生产再考虑更严格的镜像签名、流水线、健康门禁和监控。蓝绿部署本身没那么神秘,关键就是别把“切换”想得太高级,它很多时候就是换一个 upstream,然后 reload Nginx。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。