Docker Compose 热更新必现 502:排障与回滚实践
单机 Docker Compose 热更新必现 502 的根因(Nginx upstream 不重解析)与 Cloudflare IPv6 偶发失败的排障、回滚实践。
Docker Compose 热更新必现 502?一次生产环境滚动更新的排障与回滚实践
适用场景:单机 Docker Compose 部署「前端 + 后端 + Nginx 反代」的项目,需要频繁热更新。
一、背景
一个单机 Docker Compose 项目:Nginx 反代容器 + 前端容器 + 后端容器,数据走 Postgres / Redis 容器。开发期几乎每天热更新,于是把部署流程沉淀成了一套"可读、可回滚、不动密钥"的脚本。下面是两个高频坑。
二、坑 1:重建容器必现 502
现象
docker compose up -d 重建前后端容器后,访问前端 / 调 API 偶发 502 Bad Gateway;过一会儿或手动 reload 后恢复。
根因
Nginx 的 upstream 是用容器名(Docker 内部 DNS)定义的。Nginx 在启动时解析一次 upstream IP 并缓存,不会自动重解析。容器被 up -d 重建后,新容器拿到了新的 IP,而 Nginx 仍持有指向旧容器(已退出)的失效 upstream 句柄 → 502。
# nginx.conf(典型写法,启动即解析并缓存 upstream)
upstream backend {
server backend:8000; # 解析一次,容器重建后 IP 变了但 Nginx 不知道
}
解决
每次 up -d 之后,必执行 nginx -s reload 让它重新解析 upstream。把它写进部署脚本的硬步骤:
docker compose up -d
docker exec <nginx-container> nginx -s reload # 关键:重建后必现 502 的解药
验证时盯一个指标:reload 后 502 条数应为 0。漏掉这步,前端页面能出但偶发 502,非常难查。
进阶:若想彻底免 reload,可在 Nginx 里用
resolver+ 变量形式上游(set $upstream backend; proxy_pass http://$upstream;),让 Nginx 每次请求都走 DNS 解析。但单机场景,reload 一步最简单可靠。
三、坑 2:Cloudflare IPv6 ENETUNREACH
现象
前端构建阶段 pnpm install --frozen-lockfile 偶发失败:ENETUNREACH,连接的是一个 Cloudflare 的 IPv6 地址(2606:4700::...)。
根因
构建容器内部 IPv6 不可达,而 npm registry 偶尔把域名解析到 IPv6 地址。一次失败,重试走 IPv4 慢速重试后就成功,next build 也能正常产出静态页。
解决与边界
- 构建失败 ≠ 部署失败:失败不会动
latest镜像标签,运行中的容器零影响。直接重试即可。 - 根治:在构建环境固定 registry 走 IPv4,或禁用 IPv6 解析(
/etc/resolv.conf去掉 IPv6 nameserver,或node侧family: 4)。
# 重试即可,不动运行容器
pnpm install --frozen-lockfile # IPv4 重试通过后继续
docker compose up -d
四、回滚策略(部署前的标配动作)
- 部署前打回滚标签:给当前正常镜像打
pre-<date>标签(仅引用旧镜像,近乎零空间); - 干净暂存:上传代码时排除
.env/.venv/node_modules/.next,绝不动生产密钥与生产 compose; - 构建 → up -d → nginx reload → 验证:
/health直连 200、前端出真实 HTML、502=0、容器 healthy; - 验证通过再清理:删回滚标签 +
docker builder prune回收悬空构建层,释放磁盘。
安全红线(永远不碰):运行中的容器、数据卷(PG / Redis / 业务数据目录)、.env 密钥、生产 compose 文件。
五、复盘
- 把
nginx -s reload和 打回滚标签 做成部署脚本的硬步骤,不要靠"记得"; - 构建失败不等于部署失败——latest 标签没动,运行容器就不受影响,重试或换网络环境即可;
- 回滚标签 + 构建缓存 prune 是低成本的空间回收手段,但只在验证通过后执行。
单机部署没有 K8s 的滚动更新兜底,这些"土办法"反而最稳。