502 和 504 都由网关或反向代理返回,但含义不同:502 表示代理没有从上游得到可用响应,504 表示代理等待上游响应超过时限。排查时先找到产生状态码的那一层,再从该层所在的网络访问上游。
确认哪一层返回 502 或 504
浏览器前面可能同时有 CDN、云负载均衡、Caddy 或 Nginx。先保留状态码、响应头、时间和请求 ID:
curl -sS -o /dev/null -D - \
-w 'remote=%{remote_ip} code=%{http_code} ttfb=%{time_starttransfer} total=%{time_total}\n' \
https://example.com/path
Server、Via、CDN 特征头和自定义 request ID 可以缩小范围,但不能单独证明根因。把同一时间的 CDN、入口和应用日志对齐;如果 CDN 日志显示源站 502,而源站入口没有这次请求,还要检查两者之间的地址、端口和 TLS。
RFC 9110(在新标签页打开)对两种状态的定义是:
502 Bad Gateway:作为网关或代理访问上游时,收到无效响应;504 Gateway Timeout:作为网关或代理访问上游时,没有在规定时间内收到响应。
具体软件会使用不同日志描述连接拒绝、连接重置、握手失败和响应解析失败。状态码只能指出大致范围,根因仍要从对应代理的日志中确认。
502 先查上游能不能被代理访问
docker compose ps
docker compose logs --since 10m proxy app
docker compose exec proxy getent hosts app
docker compose exec proxy curl -sv http://app:8080/healthz
在代理容器中执行最后一条命令,可以同时验证服务名解析、容器网络、端口、协议和应用响应。宿主机执行 curl localhost:8080 成功,只能证明宿主机到已发布端口可用。
Compose 默认网络中,应使用服务名和容器端口:
example.com {
reverse_proxy app:8080
}
location / {
proxy_pass http://app:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
Docker Compose 网络文档(在新标签页打开)说明服务重建后 IP 可能变化,服务名保持不变。把容器 IP 写进代理配置,会在一次正常重建后制造 502。
按日志文本定位 502
| 代理日志 | 常见原因 | 检查 |
|---|---|---|
| connection refused | 应用未监听、端口写错、启动未完成 | ss -lntp、容器日志、healthcheck |
| host not found / no such host | 服务名错误、网络不同、DNS 失败 | getent hosts app、docker inspect 网络 |
| connection reset by peer | 应用崩溃、主动断连、协议不一致 | 应用错误日志、OOM、HTTP/HTTPS 配置 |
| TLS handshake failed | 上游协议、SNI、证书信任错误 | curl -vk 只用于定位,再修复信任链 |
| invalid header / upstream sent invalid response | 上游并非 HTTP 或响应格式无效 | 直连端口、协议和应用日志 |
应用必须监听容器网络可达的地址,常见配置是 0.0.0.0:8080。监听 127.0.0.1:8080 只允许应用容器自身连接;限制公网暴露应通过宿主机端口映射和防火墙完成,而不是把容器内服务绑到回环地址。
504 先找上游在等什么
上游可连接但长时间不返回时,代理最终产生 504。从代理容器测量上游的 TTFB 和总耗时:
docker compose exec proxy curl -sS \
-o /dev/null \
-w 'code=%{http_code} ttfb=%{time_starttransfer} total=%{time_total}\n' \
http://app:8080/slow-path
docker stats --no-stream
把请求时间与应用日志、数据库慢查询、连接池等待和外部 API 调用对齐,优先检查:
- SQL 查询、锁等待或连接池耗尽;
- 外部 API 没有合理超时,重试叠加;
- CPU、内存或磁盘持续饱和;
- 大文件由应用同步生成或转发;
- 请求已经被客户端取消,上游仍继续计算;
- 多层代理的超时顺序相反,外层先于内层断开。
超时要按整条调用链设计
假设外层允许等待 30 秒,应用内部不能给数据库 30 秒、外部 API 再给 30 秒。内层超时、重试和清理需要在外层期限前完成,并给错误响应留出时间。
Nginx proxy_read_timeout(在新标签页打开)限制的是两次读取操作之间允许的时间,不等于整个响应必须在该秒数内完成。Caddy reverse_proxy(在新标签页打开)也分别提供拨号、响应头、读取和写入等传输超时。修改前先确认实际卡在哪个阶段。
仅当业务本来就是长操作时,才考虑:
- 改成异步任务,立即返回任务 ID;
- 提供进度、取消和幂等查询;
- 同时调整网关、应用和依赖的超时预算;
- 限制并发,避免长请求拖垮普通请求。
直接把代理超时从 30 秒改到 10 分钟,会增加连接占用,也可能把数据库或第三方故障隐藏得更久。
healthcheck 不能只检查进程存在
健康检查至少验证应用能够接收请求;涉及数据库的 readiness 可以确认关键依赖,但不要把昂贵查询放进高频探针。Compose 可等待依赖进入 healthy 后再启动消费者:
services:
app:
image: example/app:1.4.2
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 3s
retries: 6
容器显示 running 只代表主进程未退出,不能证明端口已监听、路由可用或依赖已就绪。
恢复后分别检查应用、代理和公网入口
docker compose exec app wget -qO- http://127.0.0.1:8080/healthz
docker compose exec proxy curl -fsS http://app:8080/healthz
curl -fsS https://example.com/healthz
再确认代理错误日志停止增长、应用错误率恢复、请求耗时回到基线,并执行一次应用重建。重建后仍通过服务名访问,才说明没有依赖旧容器 IP 或偶然网络状态。
端口到底应绑定到回环、宿主公网还是只留在 Compose 网络中,见Docker 端口、回环绑定与防火墙。
常见问题
502 和 504 的区别是什么?
502 表示代理没有得到可用上游响应;504 表示等待上游超过时限。以产生状态码的代理日志和同网络直连结果确认具体原因。
出现 504 可以直接把代理超时调大吗?
不应先调大。先查应用、数据库、外部 API 和资源争抢;长任务优先改为异步执行。
宿主机 curl 上游正常,为什么代理仍然 502?
代理容器使用不同网络命名空间。必须从代理容器请求服务名和容器端口,宿主机 localhost 不能替代这个验证。
Docker Compose 中反向代理应该连接容器 IP 吗?
不应该。使用服务名和容器端口,并让代理与应用加入同一网络。容器 IP 在重建后可能变化。
怎样确认 502 或 504 已经恢复?
从应用容器、代理容器和公网三层检查同一健康地址,并确认重建后、真实业务请求和日志指标都恢复。