适用场景:单机 Tomcat 应用(backend),固定宿主端口 8080,要求更新发布期间服务不中断。
方案:nginx(8080 入口) + 双实例(blue / green)蓝绿切换,发布时"先启新 → 探活 → 原子切换 → 删旧"。


1. 背景与目标

原问题:旧部署脚本是"先停旧、再启新"(docker stop → docker rm → docker run),存在两个缺陷:

  1. rm 之后、run 的新容器就绪之前,8080 端口无服务监听,更新期间服务必然中断;
  2. 无健康检查、无失败回滚,新镜像起不来时服务会长时间不可用。

本方案目标:

  • 发布期间零停机(切换为 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 upstreamupstream 指向的容器不存在 / 容器名与 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 Gatewayupstream 指向的实例没起 / 切换后未 reloaddocker compose ps 确认实例状态;重跑 reload
nginx 启动报端口占用旧容器仍占 8080docker 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. 运维注意事项

  1. 日志分目录:blue/green 必须挂不同 logs 目录,否则双实例并存期 Tomcat 并发写 catalina.out 会互相踩。
  2. conf 共享只读:/data/applet/backend/conf 被双实例共享,不要在运行中由应用改写,否则会影响两侧。
  3. 健康检查工具:镜像内必须预装 curl(或 wget),否则 healthcheck 形同虚设。
  4. 切换前一定等 healthy:容器起来了不代表 Tomcat 就绪,探活通过再 reload,避免瞬间 502。
  5. 旧镜像保留:发布用递增 tag(v1→v2→v3),回滚零成本;若用固定 latest 必须保留备份 tag。
  6. 防火墙:8080 端口需在安全组/iptables 放行(如原来已放行则无需处理)。

更多推荐