← 全部笔记

Docker · 5 容器网络

端口映射:宿主机的 8080 通向容器的 80

-p 8080:80 在宿主机上开了一个入口。别的机器访问宿主机的 8080 时,内核把请求的目的地址改写成容器的 80,再交给容器里的程序。

约 15 分钟 · 截至 2026-11-04,Docker Engine 29.x · Docker 实验未实测

docker run -p 8080:80 nginx

请求从另一台电脑进入宿主机的 8080,被改写后到达容器的 80 另一台电脑访问 192.168.1.20:8080。宿主机内核按 Docker 写下的发布规则做目的地址转换,把目的改为容器的 172.17.0.2:80,容器里的 nginx 在 80 端口接收。 另一台电脑 192.168.1.50 宿主机 · 192.168.1.20 发布规则由 Docker 写入,内核执行 访问 8080 DNAT:改写目的地址 → 172.17.0.2:80 转发 容器 · 172.17.0.2 80 nginx 在 80 端口监听 请求从另一台电脑进入宿主机的 8080,被改写后到达容器的 80 纵向版:另一台电脑访问宿主机 8080;内核把目的改为 172.17.0.2:80;容器里的 nginx 在 80 接收。 另一台电脑 192.168.1.50 访问 宿主机 · 192.168.1.20 8080 DNAT:改写目的地址 → 172.17.0.2:80 转发 容器 · 172.17.0.2 80 nginx 在 80 端口监听
  • 宿主机与它的端口
  • 容器与它的端口
  • 地址改写(DNAT)
图 1 冒号左边的 8080 是宿主机上的入口,右边的 80 是容器里程序监听的端口;两者之间靠内核改写目的地址接起来。地址为示意。

学完你能

先读懂规则

冒号左边是宿主机的端口,右边是容器的端口

容器有自己独立的网络:自己的 IP 地址,自己的一套端口。容器里的 nginx 监听 80,占用的是容器的 80,和宿主机上的 80 互不相干。外面的机器看不到容器的网络,只能访问宿主机。

端口发布(port publishing)就是在两者之间接一条路。完整写法是 -p [宿主机地址:]宿主机端口:容器端口[/协议],不写协议时是 TCP。-p 8080:80 的意思是:有人访问宿主机的 8080,就把请求送到容器的 80。[1]

nginx 在容器里监听 80。你把规则从 -p 8080:80 改成 -p 9090:80,要不要同时改 nginx 的配置?

不用。你改的是冒号左边,也就是宿主机上的入口;请求最终仍然送到容器的 80,nginx 还在 80 等着。只有改冒号右边时,才要确认容器里的程序真的在那个端口监听。下面可以自己试。

-p 8080:80

规则构造器:两端端口与应用监听端口 左边是宿主机入口端口,中间改写,右边是容器端口;只有容器端口等于应用监听端口时请求才能被接收。 客户端 宿主机 8080 目的端口 8080 → 80 容器 80 nginx 监听 :80 能接收 客户端 宿主机 8080 目的端口 8080 → 80 容器 80 nginx 监听 :80 能接收

客户端访问宿主机的 8080;内核把目的端口改成 80;nginx 正在 80 监听,请求能被接收。

文字对照:容器端口必须等于程序实际监听的端口;宿主机端口可以随意选一个没被占用的。

图 2 改左边只换入口;改右边时,它必须和容器里程序监听的端口一致,否则请求送到了一扇没人应的门。

所以排查“访问不通”时,第一件事是分清两个数字:宿主机端口是给客户端用的,可以按需要换;容器端口必须对准程序,不能随便写。复习镜像与容器的关系可看镜像、容器与仓库,复习 TCP 连接可看三次握手。

跟着一个包走

内核改写目的地址,请求才进得了容器

宿主机上的 8080 并不一定有程序在监听。在常见的 Linux Docker Engine(默认 bridge 网络、NAT 模式)上,Docker 把发布规则写进内核的包过滤系统。请求包到达时,内核直接改写它的目的地址,再按新地址转发给容器。

下面用示例地址跟一个包走一遍。注意看右边的包头卡片:哪一行在哪一步变了,哪一行始终没变。

请求包从客户端到 nginx 的六步 包从 192.168.1.50 发往宿主机 192.168.1.20:8080;内核 DNAT 把目的改为 172.17.0.2:80;nginx 收到时源地址仍是客户端;回程由连接跟踪把源地址改回 192.168.1.20:8080。 另一台电脑 192.168.1.50 宿主机 · 192.168.1.20 内核 DNAT 改写目的地址 容器 · 172.17.0.2 nginx 监听 80 8080 80 请求包 · 包头 源 192.168.1.50:51234 目的 192.168.1.20:8080 DNAT 之后 源 192.168.1.50:51234 目的 172.17.0.2:80 192.168.1.20:8080 nginx 看到的包 源 192.168.1.50:51234 目的 172.17.0.2:80 来源仍是那台电脑 回程 · 响应包 源 172.17.0.2:80 ↓ 连接跟踪反向改写 源 192.168.1.20:8080 目的 192.168.1.50:51234
  1. 那台电脑发出请求包:源是它自己的 192.168.1.50:51234(51234 是系统随机挑的临时端口),目的是宿主机的 192.168.1.20:8080。先猜:nginx 最终收到的包里,目的端口是 8080 还是 80?
  2. 包到达宿主机,目的端口 8080 命中了 Docker 为这次发布写下的规则。这里不需要有程序在 8080 监听,规则本身就能接住它。
  3. 内核做 DNAT:把目的从 192.168.1.20:8080 换成容器的 172.17.0.2:80。请求的内容(HTTP 那部分)一个字都没变。
  4. 宿主机按新的目的地址,把包转发进容器网络。图里省略了中间的网桥和虚拟网线,下一篇再拆开讲。先猜:如果容器里没有程序监听 80,这个包会怎样?
  5. nginx 在 80 收到请求。如果容器里没人监听 80,内核会直接拒绝连接,客户端看到“连接被拒绝”。注意源地址没变,nginx 的日志里记录的是那台电脑的真实 IP。
  6. nginx 回复。内核的连接跟踪(conntrack)记得这条连接被改写过,于是把响应包的源地址从 172.17.0.2:80 改回 192.168.1.20:8080。在那台电脑看来,它一直在和宿主机的 8080 通信。
图 3 去程只改目的地址,回程只改源地址;容器端的程序看到的是客户端的真实来源。地址为示意。

还有一条路:docker-proxy

你在宿主机上用 ss 查 8080,有时会看到一个叫 docker-proxy 的进程在监听。它是 Docker 的用户态代理:自己接住连接,再另开一条连接去容器,把数据来回搬运。dockerd 默认开启它,主要服务内核 NAT 不方便处理的情况,比如从宿主机自己访问 127.0.0.1:8080。[4][13]

内核 NAT:同一条连接,包头被改写

内核 NAT 只有一条连接 客户端 DNAT 1 条 TCP 连接 容器 :80

docker-proxy:进程接住,再转交

docker-proxy 有两条连接 客户端 proxy 2 条 TCP 连接,中间是一个进程 容器 :80
图 4 内核 NAT 改写包,连接只有一条;docker-proxy 转交数据,连接是两条。两者可以同时存在,具体走哪条取决于请求从哪来和守护进程的配置。
深入:这些规则写在哪里?

Docker 默认把规则写进 iptables,也支持 nftables 后端;两者都是 Linux 管理包过滤与地址转换的工具。理解端口映射不需要手写这些规则,但排查时你可能会在 iptables -t nat -L 的输出里看到名为 DOCKER 的链。[5]

在自己的机器上

用三条证据确认映射真的生效

未实测

下面的命令按 Docker 官方文档整理,还没有在本站维护者的机器上运行过;“预期”一栏是根据文档推导的。你在自己的 Linux 机器上跑完,输出以你的为准。

先确认 8080 没被别的程序占用,也没有同名容器 lp-port-web。然后启动一个只供本机访问的 nginx:

docker run --rm -d --name lp-port-web -p 127.0.0.1:8080:80 nginx:alpine
docker port lp-port-web
ss -lntp 'sport = :8080'
curl --noproxy '*' -fsS http://127.0.0.1:8080/ | head -n 4

这三条命令各回答一个不同的问题,单看任何一条都可能误判:

证据回答的问题预期(未实测)它证明不了什么
docker portDocker 记下了什么规则?80/tcp -> 127.0.0.1:8080规则存在,不代表程序在跑
ss -lntp宿主机上有没有进程在监听 8080?可能看到 docker-proxy,也可能什么都没有没有条目不代表映射失败,内核 NAT 不需要监听进程
curl从这里发一次真实请求,能不能拿到页面?nginx 默认页面的开头几行 HTML本机能通,不代表别的机器也能通

docker port 的输出格式可以对照官方文档里的示例。这是文档中名为 test 的容器,端口和本实验不同:[6]

docker port test
7890/tcp -> 0.0.0.0:4321
9876/tcp -> 0.0.0.0:1234

注意方向:docker port 把容器端口写在箭头左边,宿主机地址和端口写在右边,和 -p 的顺序正好相反。颜色能帮你对上。

换一个应用:它在容器里监听 3000,你希望只在宿主机本机通过 8080 访问它。补全规则。

-p 127.0.0.1::

答案是 -p 127.0.0.1:8080:3000。左边 8080 是你在宿主机上选的入口;右边必须是程序实际监听的 3000。

docker port 显示了映射;ss 没有列出 8080;本机 curl 成功拿到页面。哪个判断有证据支持?

ss 列的是进程打开的套接字。内核 NAT 改写的是包,用不着套接字。

这次请求从本机发出、访问的是回环地址,完全没有经过网卡和外部网络。

三条证据回答三个不同的问题。curl 成功直接证明了本机路径可用;ss 为空只说明没有用户态代理在监听,和内核转发不矛盾;而外部能否访问,这次实验根本没测。

Docker Desktop、远程 Engine 和 Rootless 模式不一样

Docker Desktop 把 Linux 容器放在一台虚拟机里,桌面系统和虚拟机之间还有一层端口转发,所以在 macOS 或 Windows 上用 ss 看到的不是本页描述的那一层。[8] Rootless 模式(不用管理员权限运行 Docker)也多了一层用户态网络。[12]

如果你的 Docker 命令连的是远程主机,端口发布在那台远程主机上,ss 和 curl 127.0.0.1 也要在那台机器上运行。

决定谁能进来

绑定地址决定了哪些机器能用这个入口

-p 前面可以加宿主机地址,决定这个入口开在哪个地址上。-p 127.0.0.1:8080:80 只在回环地址上开,只有宿主机自己能用;-p 0.0.0.0:8080:80 在所有 IPv4 地址上开,局域网里的机器也能通过网卡地址进来。[9]

先想一想:切到 127.0.0.1 时,另一台电脑访问 192.168.1.20:8080 会怎样?再切换下面的选项核对。

入口开在宿主机的哪个地址上?
绑定地址决定哪个入口打开 回环绑定只打开 127.0.0.1:8080,另一台电脑经网卡访问 192.168.1.20:8080 时进不来;绑定所有地址时两个入口都打开。 另一台电脑 192.168.1.50 宿主机 本机浏览器 在宿主机上 127.0.0.1:8080 经网卡访问 192.168.1.20:8080 能进入 没有入口 80 容器 另一台电脑 192.168.1.50 宿主机 本机浏览器 经网卡访问 127.0.0.1:8080 192.168.1.20:8080 能进入 没有入口 80 容器

-p 127.0.0.1:8080:80:只有宿主机自己能通过回环地址进来;另一台电脑访问 192.168.1.20:8080 时,这个地址上没有入口。

文字对照:绑定 127.0.0.1 只打开回环入口;绑定 0.0.0.0 打开所有 IPv4 地址上的入口。两种情况下转发目标都是容器的 80。

图 5 绑定地址只改变入口开在哪里,转发目标始终是容器的 80。

如果不写地址,-p 8080:80 默认开在所有地址上,通常还包括 IPv6。很多人以为不写就只给本机用,这是最常见的误解。只想本机访问时,要明确写出 127.0.0.1;需要 IPv6 回环,再加一条 -p '[::1]:8080:80'。[9]

外部暴露

绑定所有地址后,服务可能被局域网甚至公网访问到,最终能否访问还取决于路由、上游防火墙和云安全组。Docker 写的规则会绕过 ufw 的常规规则,不能因为 ufw 里“拒绝了 8080”就认为安全。[5]

按需求选绑定方式。只考虑 IPv4,不考虑额外的代理。

  • 在本机浏览器里调试网页
  • 让同一局域网里的同事打开看看
  • 只给宿主机上的脚本调用的管理接口
  • 正式对外提供的网站,前面有防火墙
127.0.0.1:只开回环入口
需要网卡上的入口:0.0.0.0 或指定网卡地址

只有本机用,就明确绑定回环,入口越少越安全。需要外部访问时,可以绑定所有地址,也可以只绑定某一块网卡的地址,并且另外检查访问控制。

可选实验:用第二台机器验证(会扩大暴露范围)

只在你控制的测试机和可信网络里做。先保持回环绑定,从第二台机器访问;再改成所有地址绑定重试。把 HOST_IP 换成 Docker 宿主机的局域网地址。

curl --noproxy '*' --connect-timeout 3 -fsS http://HOST_IP:8080/
docker stop lp-port-web
docker run --rm -d --name lp-port-web -p 0.0.0.0:8080:80 nginx:alpine

预期:回环绑定时,第二台机器的请求超时或被拒绝;改成所有地址后,若网络策略允许,应能拿到 nginx 页面。做完记得 docker stop lp-port-web 关掉入口。

版本提醒:Docker Engine 28.0.0 之前,同一二层网络里的其他主机曾能访问到回环绑定的端口。本页结论只适用于 28.0.0 及之后的版本。[9]

带走可操作的判断

访问不通时,沿着包走的路逐段排查

按图 3 的顺序,从客户端一路查到程序。每一步都有一条命令能给出证据:

  1. 客户端访问的地址对吗?必须是 Docker 所在宿主机的地址和冒号左边的端口。另一台机器上的 localhost 指向的是它自己。
  2. 规则在吗?docker port lp-port-web 里应该有你要的地址和端口。只写了 EXPOSE 不算发布。
  3. 入口开对地址了吗?本机能通、别的机器不通,先看是不是只绑定了 127.0.0.1,再查外部路由和防火墙。
  4. 程序在对的端口上吗?容器还在运行吗?docker logs lp-port-web 看程序是否启动、是否监听在冒号右边那个容器端口上。

要记住的

nginx 正常运行,规则是 -p 127.0.0.1:8080:80。你在宿主机上能打开页面,同事用宿主机的局域网地址打不开。最该先检查什么?

宿主机上已经能打开页面,说明从入口到 nginx 的 80 这一段没问题。

同事的 127.0.0.1 指向他自己的电脑,那里没有这个服务。

本机通、外部不通,问题出在入口开在哪个地址上:规则只在回环地址开了入口,网卡地址上根本没有。改 nginx 的端口解决不了入口范围的问题。

来源

核查日期:2026-11-04;适用版本:Docker Engine 29.x,Linux 上的默认 bridge 网络与 NAT 模式。图中地址均为示意。

  1. Docker:Publishing and exposing ports — 参数顺序、默认 TCP、发布与声明的区别。
  2. Docker CLI:docker container run — --publish、EXPOSE、--rm 与 -d。
  3. Docker with iptables — 端口映射的 NAT 规则、转发与连接跟踪。
  4. Docker CLI:dockerd — --userland-proxy 的用途与默认值。
  5. Docker:Packet filtering and firewalls — iptables 与 nftables 后端、Docker 与 ufw。
  6. Docker CLI:docker container port — 命令语法与本页引用的示例输出。
  7. ss(8),iproute2 手册 — -l -n -t -p 选项与过滤表达式。
  8. Docker Desktop:Networking — 虚拟机与桌面端口转发层。
  9. Docker:Port publishing and mapping — 绑定地址、默认发布到所有地址、IPv6、28.0.0 之前的回环问题。
  10. Docker Hub:nginx 官方镜像 — 默认在 80 端口提供 HTTP 服务。
  11. curl 手册 — --noproxy、-f -s -S、--connect-timeout。
  12. Docker Rootless:Networking errors — 无管理员权限模式下额外的网络层。
  13. Moby:docker-proxy 的 TCP 实现 — 接住前端连接、连接后端、双向转交数据。