在单机 Docker 环境下,如何实现像 Kubernetes 那样的“滚动更新”或“蓝绿部署”?很多开发者习惯于直接 docker compose restart,但这会导致服务在重启期间出现短暂的 502 错误。
本文将总结一套基于 Shell 脚本 + Docker Compose + Nginx 的轻量级蓝绿部署方案。该方案重点解决了两个核心问题:如何准确判断新服务已就绪,以及如何优雅地处理旧服务的剩余流量。
1. 架构设计
我们使用 蓝绿部署 (Blue/Green Deployment) 策略。
- 状态文件 (
AorB) :记录当前在线的是 A 环境还是 B 环境。 - 双容器槽位:
backend(Blue) 和backend1(Green)。 - Nginx 负载均衡:通过切换
upstream配置文件并重载 (reload),将流量瞬间切换到新容器。
2. 核心挑战与解决方案
在脚本演进过程中,我们解决了以下三个生产级痛点:
痛点一:服务“假启动”导致 502
旧方案:通过 grep "Worker ready" 检查日志。
问题:容器启动且打印了日志,并不代表 Web Server 已经绑定端口并准备好接收 TCP 连接。且日志缓冲会导致判断延迟。
新方案:主动 HTTP 探测 (Active Probing)
我们在脚本中引入了 wait_for_health 函数,通过 curl 持续请求应用的 /api/user/me 接口。
- 判定逻辑:只要返回 HTTP 200 或 401 Unauthorized,即视为服务可用。
- 为什么接受 401? :
/api/user/me需要登录。如果我们收到 401 错误,说明 Nginx/Gunicorn/Spring Boot 等应用层已经完全启动并拦截了请求,这正是我们需要的“健康”信号。
痛点二:暴力停机切断用户连接
旧方案:Nginx reload 后立即 docker stop 旧容器。
问题:Nginx 的 reload 是异步的,且旧容器上可能还有正在处理的长连接(如下载、WebSocket)。直接停止会导致用户端连接重置。
新方案:连接耗尽 (Connection Draining)
- Nginx 层面:设置
worker_shutdown_timeout 60s;,给旧 Worker 进程 60 秒时间处理剩余请求。 - 脚本层面:引入
wait_for_draining函数。使用 Linux 原生命令ss(Socket Statistics) 监控指向旧容器 IP 的 TCP 连接。只有当连接数归零或超时,才执行停机。
痛点三:Nginx 配置指令作用域错误
问题:worker_shutdown_timeout 被错误放置在 http 块中。
修正:该指令属于全局配置,必须放在 nginx.conf 的最外层 (Main Context) 。
3. 最终实施方案
3.1 Nginx 全局配置 (nginx.conf)
Nginx
user nginx;
worker_processes auto;
# 【关键配置】放在最外层,控制旧进程最长存活时间
worker_shutdown_timeout 60s;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
include /etc/nginx/conf.d/*.conf;
# 你的 upstream 配置文件被包含在这里
include /etc/nginx/upstream.conf;
}
3.2 自动化部署脚本 (deploy.sh)
这是整合了所有优化逻辑的完整脚本:
Bash
#!/usr/bin/env bash
# === 配置区 ===
ABFILE=nginx/AorB
CONTAINER_PORT=8000 # 容器内部应用端口
HEALTH_PATH="/api/user/me" # 健康检查接口
MAX_RETRIES=30 # 健康检查最大重试次数
DRAIN_TIMEOUT=60 # 流量耗尽最大等待秒数
# =============
# 颜色定义
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
# 初始化状态文件
if [ ! -f "$ABFILE" ]; then echo "A" > "$ABFILE"; fi
AorB=$(cat ${ABFILE})
# 获取容器内部 IP
get_container_ip() {
local id=$(docker compose ps -q $1 2>/dev/null)
if [ -n "$id" ]; then
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $id
fi
}
# 1. 健康检查:等待 API 返回有效响应 (包括 401)
wait_for_health() {
local service=$1
local count=0
echo -e "${YELLOW}🔍 Checking health for $service...${NC}"
while [ $count -lt $MAX_RETRIES ]; do
local ip=$(get_container_ip $service)
if [ -n "$ip" ]; then
# 获取响应内容
local resp=$(curl -s --max-time 2 "http://${ip}:${CONTAINER_PORT}${HEALTH_PATH}")
# 只要包含 Unauthorized,说明服务活着
if echo "$resp" | grep -q "Unauthorized"; then
echo -e "${GREEN}✅ $service is READY!${NC}"
return 0
fi
fi
sleep 2
count=$((count + 1))
done
echo -e "${RED}❌ Health check failed for $service${NC}"; return 1
}
# 2. 流量耗尽:等待旧连接断开
wait_for_draining() {
local service=$1
local count=0
local ip=$(get_container_ip $service)
[ -z "$ip" ] && return 0
echo -e "${YELLOW}🛑 Draining connections for $service ($ip)...${NC}"
while [ $count -lt $DRAIN_TIMEOUT ]; do
# 检查目标为旧容器 IP 的 ESTABLISHED 连接
local conn=$(ss -tn state established dst $ip | grep -v Recv-Q | wc -l)
if [ "$conn" -eq "0" ]; then
echo -e "${GREEN}✅ No active connections. Safe to stop.${NC}"
return 0
fi
echo " Active connections: $conn. Waiting..."
sleep 1
count=$((count + 1))
done
echo -e "${RED}⚠️ Timeout! Force stopping.${NC}"
}
# 3. 启动并切换
deploy() {
local new=$1
local old=$2
local env=$3
echo "=== Deploying $new (Replace $old) ==="
cp envs/prod-common.env "$env"
# A. 启动新容器
docker compose build $new
docker compose up -d $new
# B. 健康检查
wait_for_health $new || exit 1
# C. Nginx 切换流量
cp nginx/$new.conf.tmpl nginx/upstream.conf
sudo nginx -t && sudo nginx -s reload
echo -e "${GREEN}🚀 Traffic switched to $new${NC}"
# D. 旧节点处理 (流量耗尽 -> 停止)
wait_for_draining $old
docker compose stop $old
}
# 主流程
if [ "$AorB" == "A" ]; then
deploy "backend" "backend1" "envs/prod.env"
echo B > ${ABFILE}
else
deploy "backend1" "backend" "envs/prod1.env"
echo A > ${ABFILE}
fi
4. 总结
这套脚本实现了一个闭环的自动化发布流程:
- Start: 启动新容器。
- Verify: 通过 HTTP 语义(401 Unauthorized)确认业务逻辑已加载。
- Switch: Nginx 热加载,流量切换。
- Drain: 监控 TCP 连接,等待旧请求处理完毕。
- Stop: 安全关闭旧容器。
这种方案不需要复杂的 Kubernetes 运维知识,非常适合中小规模的 Docker Compose 部署场景,能显著提升发布的稳定性和用户体验。