nginx 里写 localhost 反而 502?一条请求带你彻底搞懂 Docker 端口映射与反向代理

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 网络最经典的下一课:容器之间怎么互相访问。


相关推荐
非优秀程序员1 天前
题记|《互联网产品恩仇录》
后端·程序员·创业
知已难寻1 天前
Docker 入门:容器技术与 Docker 基础
java·运维·服务器·docker·云原生·容器
xbzb1 天前
镜像到底存在哪:/var/lib/docker 的四级映射与 daemon.json 配置
docker·容器·json·镜像·daemon.json·四级
易师傅1 天前
技术面试已死?AI 时代,真正拉开程序员差距的是什么?
前端·后端·程序员
Hilaku1 天前
你们公司有 35 岁以上的前端吗?
前端·javascript·程序员
AI前端工程狮1 天前
服务端已经死了,客户端却报"超时"
人工智能·程序员
楠风测开1 天前
两个任务同时跑,B 的登录态为什么被踢回登录页
程序员
codigger1 天前
大厂git提交规范
git·程序员
挖掘狂人1 天前
六条分支,一条主干:我的 Git 分支管理心得
git·后端·程序员
方便面不加香菜1 天前
Docker Volume -- 管理卷Volume详解
运维·docker·容器