本文是「Nginx Docker 避坑」系列第一篇,本系列共四篇
后续篇目:反向代理 502/504 排查 → SSL 与 WebSocket → 生产运维与镜像管理
我在微信群里看到同样的问题,少说出现了几十次。
有人说 Nginx 里写了 upstream,容器跑起来了,就是 502。有人说端口映射了 80,访问死活不通。还有人说昨晚弄到凌晨两点,最后发现改了一行配置重启就好了,但不知道为什么。
这些问题,根子上都出在容器网络这一块。
今天这篇,把我踩过的那几个最坑的地方,完完整整梳理一遍。
先说最经典的那个误解
很多人第一次用 Docker 跑 Nginx 做反向代理,会这么写配置:
upstream backend {
server localhost:3000;
}
然后发现,完全不通。
curl 一下,连接拒绝。看日志,满屏 502。
为什么?
因为这里有一个很多人没意识到的基础概念:容器里的 localhost,指的是容器自己,不是你的宿主机。
Docker 每个容器都有独立的网络命名空间,就好比每个容器住在一个隔离的小房间里,localhost 就是这个小房间的内部地址。你的 Node.js 服务跑在宿主机上,对容器来说是"另一台机器",跟你从家里连公司服务器没区别。
这个认知如果没建立起来,后面踩再多坑也是白踩。
那怎么写才对?
根据你的部署场景,有三种解法,从将就到正规:
🔧 方法一:用宿主机的网关 IP(临时方案)
upstream backend {
server 172.17.0.1:3000;
}
172.17.0.1 通常是 Docker bridge 网络的网关地址,也就是宿主机在这个虚拟网络里的 IP。大部分情况下这个地址是固定的,可以用,但不够优雅。
Docker 20.10 之后有更好的写法,启动容器时加一个参数:
docker run --add-host=host-gateway:host-gateway nginx
然后配置里写:
upstream backend {
server host-gateway:3000;
}
🔧 方法二:用 host 网络模式(开发图省事用)
docker run --network=host nginx
这样容器直接共用宿主机的网络,localhost 就真的是宿主机了。图省事可以,生产环境不推荐,失去了容器隔离的意义。
✅ 方法三:把所有服务都容器化,走 Docker 内部网络(生产标准)
services:
nginx:
image: nginx:1.26.2-alpine
networks:
- app-network
backend:
image: your-node-app
networks:
- app-network
networks:
app-network:
driver: bridge
然后 Nginx 配置直接用服务名:
upstream backend {
server backend:3000;
}
这才是最干净的玩法。Docker 有内置 DNS,服务名就是 hostname,IP 变了它自己解析,你不需要关心。
能用方法三,就不要用方法一二。
容器 IP 不要写死,这不是建议
有人习惯先 docker inspect 看一下容器 IP,比如看到 172.17.0.3,然后写进配置文件里。
能跑,但迟早出问题。
容器一重启,IP 就变了。今天 172.17.0.3,重启后可能变成 172.17.0.5,然后你就开始找为什么好好的服务突然 502 了,翻半天才发现是 IP 变了。
在同一个 Docker 网络里,永远用服务名通信,不要用 IP。
一个细节:默认网络和自定义网络不一样
很多人不知道这个差别,但它很关键。
Docker 有一个默认的 bridge 网络,也叫 docker0。你直接 docker run 不加任何网络参数,容器就进了这个默认网络。
默认 bridge 里的容器,无法通过服务名互相解析,只能用 IP。
只有自定义 bridge 网络才支持 DNS 服务发现,才能用服务名通信。
Docker Compose 之所以能直接用服务名,就是因为 Compose 默认帮你创建了一个自定义 bridge。但你如果手动 docker run 放进默认网络,就享受不到这个特性。
结论很简单:生产环境别用默认网络,自己建一个。
docker network create app-network
`depends_on` 不是你想的那样
这个坑很多人栽进去过,我也是。
services:
nginx:
depends_on:
- backend
看起来 Nginx 会等 backend 好了再启动,但实际上 depends_on只保证容器启动了,不保证服务就绪了。
Node.js 应用可能还在连数据库,Java 应用可能热身要三十秒,容器是"起来了",但服务还没准备好接请求,这时候 Nginx 上线就遇到 upstream 不可用,照样出问题。
正确的写法是配合健康检查:
services:
nginx:
depends_on:
backend:
condition: service_healthy # 等健康检查通过
backend:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
start_period 给服务留足启动时间,retries 控制重试次数,这才是生产级的启动依赖写法。
端口映射那些让人迷糊的事
坑一:address already in use
Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use
每个人都见过这行报错。排查很简单:
sudo ss -tlnp | grep :80
看看是谁占了 80 端口。常见的是宿主机上直接装的 Nginx 或 Apache。
sudo systemctl stop nginx
sudo systemctl disable nginx # 长期用 Docker,顺手禁掉自启
坑二:映射了端口,Nginx 里该写哪个
-p 8080:80 这个映射,很多人以为 Nginx 里要改成 listen 8080。
错。
-p 8080:80 的意思是:把宿主机的 8080,映射到容器内部的 80。Nginx 在容器里,它根本感知不到宿主机用的什么端口,它只需要监听容器内部的 80:
server {
listen 80; # 写容器内部端口,和宿主机无关
}
坑三:只绑定了 IPv6
有时候你明明映射了端口,某些客户端访问就是不通。检查一下:
ss -tlnp | grep :80
# 如果只看到 :::80,没有 0.0.0.0:80,就是只绑了 IPv6
启动时明确指定 IPv4:
docker run -p 0.0.0.0:80:80 nginx
最后一个隐藏坑:upstream DNS 缓存
这个问题藏得比较深,不是所有人都会遇到,但遇到了会很困惑。
Nginx 启动时会把 upstream 里的服务名解析成 IP 并缓存下来。如果 backend 容器重启了,IP 变了,Nginx 不知道,还在用旧 IP,结果请求全 502 了。但你看 backend 服务是好的,看 Nginx 也在跑,就是不通,百思不得其解。
解决方式是加 resolver 指令,并且用变量传给 proxy_pass:
http {
resolver 127.0.0.11 valid=30s; # Docker 内置 DNS
server {
set $backend_url http://backend:3000;
location /api/ {
proxy_pass $backend_url; # 用变量,运行时动态解析
}
}
}
proxy_pass 接收变量时,Nginx 会在每次请求时重新解析 DNS,而不是启动时缓存一次。这个细节很多文章都没提,但在容器环境里相当重要。
快速排查命令
遇到问题先跑这几条,基本能定位:
# 容器在哪个网络?
docker network ls
docker inspect nginx-container | grep -A5 Networks
# 网络里有哪些容器?
docker network inspect app-network
# 从 Nginx 容器内部测试连通性(这步最关键)
docker exec -it nginx-container curl http://backend:3000/health
# 端口占用情况
sudo ss -tlnp | grep :80
# 容器端口映射
docker port nginx-container
总结一下
容器网络这块,记住三件事就够了:
① 容器里的 localhost 是容器自己,不是宿主机
② 生产用自定义 bridge 网络,不用默认 docker0
③ upstream 服务名不会自动更新,要用 resolver + 变量做动态解析
这三条搞清楚,80% 的"打不通"问题就不会发生了。
下一篇讲配置语法和反向代理。proxy_pass 末尾加不加斜杠,是两种完全不同的行为,这个坑我见过不止一百次了。
如果这篇对你有帮助,转发给还在踩这些坑的同事吧。
阅读原文:原文链接
该文章在 2026/3/27 16:19:11 编辑过