nginx 里写 localhost 反而 502?一条请求带你彻底搞懂 Docker 端口映射与反向代理
先看一段配置------这是一个 nginx 容器,用来反向代理本机 1314 端口上的 Node 服务:
nginx
# nginx.conf
events {}
http {
server {
listen 80;
location / {
proxy_pass http://localhost:1314; # ⚠️ 这里写了 localhost
proxy_set_header Host $host;
}
}
}
启动它,浏览器打开 http://localhost,你看到的是:
502 Bad Gateway
可你的 Node 服务明明在 1314 端口跑得好好的,curl http://localhost:1314 也通。为什么 nginx 一代理就 502?
我把这个坑调明白之后才发现:502 的根因不是配置格式写错,而是我根本没搞懂「一条请求在 Docker 里是怎么走的」。 这篇文章就把这条路走一遍------搞清楚之后,这种坑你以后一眼就能看出来。
一、Docker 到底解决了什么问题
Docker 解决的是「我电脑能跑,你电脑怎么跑?」的问题。
你接手一个三年前的 Vue2 项目,要求 Node 16 + npm 8,可你电脑装的是 Node 22------装回旧版本吧,别的项目又炸了。代码之外,一个应用其实还「依赖」一堆有版本要求的运行环境:Node、Redis、MySQL......
Docker 的思路是:把应用和它需要的运行环境打包成一个整体,在任何设备上都能原样跑起来。就像海运的集装箱------不管里面装的是红酒还是毛衣,港口都按「一个标准箱子」来处理。
Docker = 应用 + 运行环境(打包成一个标准集装箱)
这一句就够了。剩下两个概念是它的零件:
| 概念 | 比喻 | 是什么 |
|---|---|---|
| 镜像 image | 光盘 | 只读模板:应用 + 环境,打包好的一整套 |
| 容器 container | DVD 播放机 | 镜像运行起来之后的实例,可启动 / 停止 / 删除 |
同一个镜像(光盘)可以同时起多个容器(多台播放机放同一张盘),互不影响。
概念点到为止。真正的重头戏是:把 nginx 装进容器之后,一条请求是怎么从浏览器走到 Node 应用的?
二、一条请求的旅程:从浏览器到 Node
先交代我们这个 demo 的「地形」------注意,它刻意做了一个很常见的混搭:
- Node 服务跑在宿主机上 ,监听
0.0.0.0:1314 - nginx 跑在 Docker 容器里,监听 80
- nginx 负责把 80 的请求,转发给宿主机的 1314
这种「一半在容器里、一半在容器外」的部署,恰恰是最容易踩 502 的。先把路线图看清楚:
text
浏览器
│ http://localhost (默认就是 80 端口)
▼
宿主机 :80 ─────────────────────┐
│ ① -p 80:80 端口映射(宿主 80 → 容器 80)
┌──────────────────────────────▼──────────────┐
│ Docker 容器 │
│ │
│ nginx :80 (listen 80) │
│ │ │
│ │ ② proxy_pass 反向代理 │
│ ▼ │
└──────┼───────────────────────────────────────┘
│ ③ host.docker.internal:1314 从容器回到宿主机
▼
宿主机 Node :1314 ──► "hello world"
一张图,三个关键词,就是这篇文章的全部。下面一个一个拆。
① -p 80:80:端口映射,谁映射谁?
启动 nginx 容器的命令长这样:
bash
docker run --name my-nginx-demo \
-p 80:80 \
-v /path/to/demo/nginx.conf:/etc/nginx/nginx.conf \
-d nginx
-p 80:80 的读法是 「宿主机端口 : 容器端口」------前面是外面,后面是里面:
text
-p 80:80 = 宿主机 80 ──转发──▶ 容器内 80
↑ 外面 ↑ 里面
为什么需要它? 因为容器有自己独立的网络,容器里的 80 端口默认在外面够不着。-p 就是开一扇门,把「宿主机的某个端口」和「容器的某个端口」对上。
记法:
-p冒号左边永远是你(宿主机),右边是容器。谁在外面谁在左。
② -v:卷挂载,让容器用你的配置文件
-v 本机路径:容器路径 把宿主机的一个文件 / 目录「映射」进容器。
这里把本机的 nginx.conf 挂到容器里 nginx 默认读配置的位置 /etc/nginx/nginx.conf。这样你改本机的配置,容器里的 nginx 直接生效,不用每次重新 build 镜像。
-p 和 -v 是 docker run 最高频的两个参数,记住冒号左边都是「你(宿主机)」就够了。
③ proxy_pass 反向代理,以及那个 502 的真相
现在到最关键的一步。nginx 收到 80 的请求后,proxy_pass 把它转发给谁?
nginx
location / {
proxy_pass http://host.docker.internal:1314; # ✅ 正确
}
为什么写 host.docker.internal,而不是 localhost?这就是开头 502 的真相。
容器有自己的 localhost。 容器是一个独立的小世界,它内部的 localhost 指向它自己------而容器里根本没有 1314 端口的服务(Node 在宿主机上呢)。所以 nginx 写 localhost:1314 时,是在「容器内部」找 1314,找不到 → 连接被拒 → 返回 502。
nginx
proxy_pass http://localhost:1314; # ❌ 在容器里找 1314,什么都没有 → 502
proxy_pass http://host.docker.internal:1314; # ✅ 穿过容器边界,回到宿主机找 1314
host.docker.internal 是 Docker 提供的一个特殊域名,专门用来让容器访问宿主机。它不是真实的 DNS 记录,而是 Docker 在容器里注入的一个别名,指向宿主机地址。
⚠️ 一个平台的坑:这个域名在 Docker Desktop(Mac / Windows)上开箱即用,Linux 上默认没有 ,得加
--add-host=host.docker.internal:host-gateway才认。
三、为什么叫「反向」代理?和正向代理差在哪
你可能会想:nginx 不就是个转发吗,干嘛叫「反向」?关键在代理的是谁。
| 正向代理 | 反向代理 | |
|---|---|---|
| 代理对象 | 客户端(你的请求) | 服务端(后端服务) |
| 典型场景 | 公司 / 梯子代理服务器 | nginx、网关 |
| 客户端是否知情 | 知道,要手动配代理 | 不知道,以为访问的就是服务 |
| 一句话 | 帮「你」上网 | 帮「服务器」接客 |
- 正向代理:浏览器配一个代理服务器,请求先到代理,再由代理去访问真正的网站。代理站在「客户端」这边。
- 反向代理 :客户端访问
http://localhost(80 端口),它以为这就是服务本身;实际上 nginx 在背后把请求转给了 1314 的 Node。客户端完全不知道后端在哪个端口、哪台机器。
「localhost 我们是不知道后端具体在哪个端口上运行的」------这就是反向代理的精髓。
为什么服务端要藏起来?因为这样后端可以随便换端口、加机器做负载均衡、加缓存、做 HTTPS 终结,而客户端无感知。80 是 HTTP 默认端口,用户不用记 :1314 这种尾巴。
四、动手复现:把 localhost 换成 host.docker.internal
光讲原理不算数,你复制这套代码跑一遍,能看到一模一样的结果。先准备一个最小 Node 服务:
javascript
// index.js ------ 宿主机上的最小 HTTP 服务
const http = require('http');
const server = http.createServer((req, res) => {
res.end('hello world');
});
// 🔑 关键:监听 0.0.0.0 而不是 127.0.0.1
// 0.0.0.0 会绑定所有网卡,容器才能通过 host.docker.internal 访问到它;
// 只监听 127.0.0.1 的话,请求会被挡在宿主机的回环地址上。
server.listen(1314, '0.0.0.0', () => {
console.log('server is running on port 1314');
});
启动它,然后分别用两版 nginx 配置跑容器。
第一版:localhost(错)
nginx
proxy_pass http://localhost:1314;
bash
node index.js # 宿主机 1314 跑起来
docker run --name nginx-bad -p 80:80 \
-v /path/to/nginx-bad.conf:/etc/nginx/nginx.conf -d nginx
curl http://localhost
你会看到:
xml
<html><head><title>502 Bad Gateway</title></head>...
而 docker logs nginx-bad 里的报错一针见血:
scss
connect() failed (111: Connection refused) while connecting to upstream
Connection refused 说明 nginx 连 1314 都没连上 ------因为它在容器内部找 localhost:1314,那里什么都没有。
第二版:host.docker.internal(对)
nginx
proxy_pass http://host.docker.internal:1314;
bash
docker run --name nginx-good -p 80:80 \
-v /path/to/nginx-good.conf:/etc/nginx/nginx.conf -d nginx
curl http://localhost
hello world
两版配置只差一个词,结果一个是 502、一个是 200。这个对比就是这篇文章唯一想让你记住的东西。
五、几个 docker 命令收尾
顺带把 docker 运维常用的命令记一下:
bash
docker pull nginx # 拉取镜像(像 git pull)
docker run ... nginx # 跑一个容器
docker stop $(docker ps -q) # 停掉所有容器
docker rm $(docker ps -aq) # 删掉所有容器
docker rmi nginx # 删掉镜像
结尾
回到开头那个 502:nginx 写 localhost 会 502,不是配置格式错了,而是 localhost 在容器里指的是容器自己。 记住这张图就够了:
text
浏览器 ──:80──▶ 宿主机 ──(-p 80:80)──▶ 容器内 nginx:80 ──(proxy_pass)──▶ host.docker.internal:1314 ──▶ Node
下次再遇到「容器里的东西访问不到宿主机」,第一反应就应该是:你写的 localhost,是不是其实指向了容器自己? 该用 host.docker.internal 的时候,别用 localhost。
留一个问题给你:如果 Node 服务也 装进另一个容器(而不是跑在宿主机上),proxy_pass 里该写什么?host.docker.internal 还对不对?欢迎在评论区聊聊------这是 Docker 网络最经典的下一课:容器之间怎么互相访问。