Docker 容器重建后 Nginx 502 问题排查与解决
问题现象
在 Docker 环境中部署了多个服务,使用 Nginx 作为反向代理。当通过拉取新镜像并用 Docker-Compose 重新创建前端容器后,访问页面出现 502 Bad Gateway 错误。
Nginx 容器本身运行正常,未重启、未修改配置,只是下游的前端容器被重建了,就出现了 502。
环境信息
- 部署方式:Docker Compose
- Nginx 容器名称:
ai-xx-nginx - Nginx 配置中通过容器名代理转发,例如:
nginx
location / {
proxy_pass http://ai-xx-front/;
}
问题分析
根本原因:Nginx DNS 缓存
Nginx 在处理 proxy_pass 时,如果直接写的是域名(包括 Docker 容器名),只在启动或重载时解析一次 DNS,之后将解析结果缓存起来。
整个流程如下:
ai-xx-front 新容器 用户 ai-xx-front 旧容器 Docker DNS (127.0.0.11) Nginx 容器 ai-xx-front 新容器 用户 ai-xx-front 旧容器 Docker DNS (127.0.0.11) Nginx 容器 #mermaid-svg-I2VPeOpb785ZRNkJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-I2VPeOpb785ZRNkJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-I2VPeOpb785ZRNkJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-I2VPeOpb785ZRNkJ .error-icon{fill:#552222;}#mermaid-svg-I2VPeOpb785ZRNkJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-I2VPeOpb785ZRNkJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-I2VPeOpb785ZRNkJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-I2VPeOpb785ZRNkJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-I2VPeOpb785ZRNkJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-I2VPeOpb785ZRNkJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-I2VPeOpb785ZRNkJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-I2VPeOpb785ZRNkJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-I2VPeOpb785ZRNkJ .marker.cross{stroke:#333333;}#mermaid-svg-I2VPeOpb785ZRNkJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-I2VPeOpb785ZRNkJ p{margin:0;}#mermaid-svg-I2VPeOpb785ZRNkJ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I2VPeOpb785ZRNkJ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-I2VPeOpb785ZRNkJ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-I2VPeOpb785ZRNkJ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-I2VPeOpb785ZRNkJ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-I2VPeOpb785ZRNkJ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-I2VPeOpb785ZRNkJ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-I2VPeOpb785ZRNkJ .sequenceNumber{fill:white;}#mermaid-svg-I2VPeOpb785ZRNkJ #sequencenumber{fill:#333;}#mermaid-svg-I2VPeOpb785ZRNkJ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-I2VPeOpb785ZRNkJ .messageText{fill:#333;stroke:none;}#mermaid-svg-I2VPeOpb785ZRNkJ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I2VPeOpb785ZRNkJ .labelText,#mermaid-svg-I2VPeOpb785ZRNkJ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-I2VPeOpb785ZRNkJ .loopText,#mermaid-svg-I2VPeOpb785ZRNkJ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-I2VPeOpb785ZRNkJ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-I2VPeOpb785ZRNkJ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-I2VPeOpb785ZRNkJ .noteText,#mermaid-svg-I2VPeOpb785ZRNkJ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-I2VPeOpb785ZRNkJ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I2VPeOpb785ZRNkJ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I2VPeOpb785ZRNkJ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I2VPeOpb785ZRNkJ .actorPopupMenu{position:absolute;}#mermaid-svg-I2VPeOpb785ZRNkJ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-I2VPeOpb785ZRNkJ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I2VPeOpb785ZRNkJ .actor-man circle,#mermaid-svg-I2VPeOpb785ZRNkJ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-I2VPeOpb785ZRNkJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段一:Nginx 启动,解析并缓存 DNS 缓存 ai-xx-front → 172.18.0.5(永不过期) 阶段二:Docker-Compose 重建前端容器 Nginx 未重启,仍使用缓存旧 IP 阶段三:用户再次访问,触发 502 阶段四:修复后(resolver + 变量) resolver 127.0.0.11 valid=5sproxy_pass $upstream(变量方式) 解析 ai-xx-front返回 IP: 172.18.0.5访问页面转发请求到 172.18.0.5200 OK正常返回页面旧容器销毁,IP 172.18.0.5 失效新容器启动,注册新 IP 172.18.0.8访问页面转发请求到 172.18.0.5(缓存)连接失败,容器已不存在502 Bad Gateway访问页面重新解析 ai-xx-front(缓存已过期)返回新 IP: 172.18.0.8转发请求到 172.18.0.8200 OK正常返回页面
关键点: Docker 容器重建后 IP 会变化,但 Nginx 仍然使用旧 IP 转发请求。
解决方案
需要同时做两处修改,缺一不可:
1. 配置 DNS 解析器
在 server 块顶部添加:
nginx
resolver 127.0.0.11 valid=5s ipv6=off;
127.0.0.11:Docker 内置 DNS 服务器的固定地址valid=5s:DNS 缓存 5 秒过期,确保容器重建后能快速感知新 IPipv6=off:关闭 IPv6,避免不必要的解析
2. 使用变量触发运行时解析
将 proxy_pass 从直接写域名改为通过变量引用:
nginx
# 修改前
location / {
proxy_pass http://ai-xx-front/;
}
# 修改后
location / {
set $upstream http://ai-xx-front;
proxy_pass $upstream;
}
为什么两者必须同时修改?
| 只加 resolver | 只用变量 | 两者都加 |
|---|---|---|
| 无效。Nginx 对直接写域名的 proxy_pass 只在启动时解析一次,不会触发 resolver | 报错 no resolver defined to resolve ai-xx-front |
正常工作 |
- 变量是触发运行时 DNS 解析的开关
- resolver 告诉 Nginx 用哪个 DNS 服务器、缓存多久
完整配置示例
nginx
server {
listen 443;
server_name localhost;
# 配置 Docker 内部 DNS,缓存 5 秒
resolver 127.0.0.11 valid=5s ipv6=off;
ssl on;
ssl_certificate /nginx/cert/server.crt;
ssl_certificate_key /nginx/cert/server.key;
# 前端服务
location / {
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 Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
add_header Access-Control-Allow-Origin *;
set $upstream http://ai-xx-front;
proxy_pass $upstream;
}
# API 网关(带路径重写)
location ^~/api/ {
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 Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
add_header Access-Control-Allow-Origin *;
set $upstream http://gateway:30080;
rewrite ^/api/(.*)$ /$1 break;
proxy_pass $upstream;
}
}
注意 :当
proxy_pass使用变量后,Nginx 不再自动处理 URI 路径替换。如果原始配置中proxy_pass的路径替换了 location 前缀(如proxy_pass http://gateway:30080/;会去掉/api/前缀),需要通过rewrite手动实现相同的路径重写逻辑。
微前端子应用的代理方式前后对比
修改前:
nginx
location /h-xx-system {
add_header 'Access-Control-Allow-Origin' '*';
proxy_pass http://h-xx-system/;
}
location /system-child {
add_header 'Access-Control-Allow-Origin' '*';
proxy_pass http://h-xx-system/system-child;
}
修改后:
nginx
location /h-xx-system {
add_header 'Access-Control-Allow-Origin' '*';
set $upstream http://h-xx-system;
rewrite ^/h-xx-system/?(.*)$ /$1 break;
proxy_pass $upstream;
}
location /system-child {
add_header 'Access-Control-Allow-Origin' '*';
set $upstream http://h-xx-system;
rewrite ^/system-child(.*)$ /system-child$1 break;
proxy_pass $upstream;
}
性能影响
valid=5s 是否意味着每次请求都要解析 DNS?
不会。 DNS 解析结果会被缓存 5 秒,在这 5 秒内的所有请求直接使用缓存结果:
时间轴:
0s ──> 解析 DNS,缓存 5 秒
1s ──> 请求到达,使用缓存 ✓(不解析)
2s ──> 请求到达,使用缓存 ✓(不解析)
3s ──> 请求到达,使用缓存 ✓(不解析)
5s ──> 缓存过期
6s ──> 请求到达,重新解析 DNS,再缓存 5 秒
最多每 5 秒才做一次 DNS 查询,对性能影响可以忽略不计。
验证方法
修改配置后,按以下步骤验证:
bash
# 1. 检查 Nginx 配置语法
nginx -t
# 2. 重新加载配置
nginx -s reload
# 3. 重建下游容器
docker-compose up -d --force-recreate ai-xx-front
# 4. 立即访问页面,确认不再出现 502
curl -k https://localhost/
总结
| 项目 | 说明 |
|---|---|
| 问题现象 | Docker 容器重建后 Nginx 返回 502 |
| 根本原因 | Nginx 启动时缓存 DNS 解析结果,容器重建后 IP 变化但 Nginx 未感知 |
| 解决方案 | 配置 resolver + proxy_pass 使用变量 |
| 性能影响 | 几乎无影响,DNS 缓存 5 秒内复用 |