先读懂规则
冒号左边是宿主机的端口,右边是容器的端口
容器有自己独立的网络:自己的 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;内核把目的端口改成 80;nginx 正在 80 监听,请求能被接收。
文字对照:容器端口必须等于程序实际监听的端口;宿主机端口可以随意选一个没被占用的。
所以排查“访问不通”时,第一件事是分清两个数字:宿主机端口是给客户端用的,可以按需要换;容器端口必须对准程序,不能随便写。复习镜像与容器的关系可看镜像、容器与仓库,复习 TCP 连接可看三次握手。
跟着一个包走
内核改写目的地址,请求才进得了容器
宿主机上的 8080 并不一定有程序在监听。在常见的 Linux Docker Engine(默认 bridge 网络、NAT 模式)上,Docker 把发布规则写进内核的包过滤系统。请求包到达时,内核直接改写它的目的地址,再按新地址转发给容器。
下面用示例地址跟一个包走一遍。注意看右边的包头卡片:哪一行在哪一步变了,哪一行始终没变。
- 那台电脑发出请求包:源是它自己的
192.168.1.50:51234(51234 是系统随机挑的临时端口),目的是宿主机的192.168.1.20:8080。先猜:nginx 最终收到的包里,目的端口是 8080 还是 80? - 包到达宿主机,目的端口 8080 命中了 Docker 为这次发布写下的规则。这里不需要有程序在 8080 监听,规则本身就能接住它。
- 内核做 DNAT:把目的从
192.168.1.20:8080换成容器的172.17.0.2:80。请求的内容(HTTP 那部分)一个字都没变。 - 宿主机按新的目的地址,把包转发进容器网络。图里省略了中间的网桥和虚拟网线,下一篇再拆开讲。先猜:如果容器里没有程序监听 80,这个包会怎样?
- nginx 在 80 收到请求。如果容器里没人监听 80,内核会直接拒绝连接,客户端看到“连接被拒绝”。注意源地址没变,nginx 的日志里记录的是那台电脑的真实 IP。
- nginx 回复。内核的连接跟踪(conntrack)记得这条连接被改写过,于是把响应包的源地址从
172.17.0.2:80改回192.168.1.20:8080。在那台电脑看来,它一直在和宿主机的 8080 通信。
还有一条路:docker-proxy
你在宿主机上用 ss 查 8080,有时会看到一个叫 docker-proxy 的进程在监听。它是 Docker 的用户态代理:自己接住连接,再另开一条连接去容器,把数据来回搬运。dockerd 默认开启它,主要服务内核 NAT 不方便处理的情况,比如从宿主机自己访问 127.0.0.1:8080。[4][13]
内核 NAT:同一条连接,包头被改写
docker-proxy:进程接住,再转交
深入:这些规则写在哪里?
Docker 默认把规则写进 iptables,也支持 nftables 后端;两者都是 Linux 管理包过滤与地址转换的工具。理解端口映射不需要手写这些规则,但排查时你可能会在 iptables -t nat -L 的输出里看到名为 DOCKER 的链。[5]
在自己的机器上
用三条证据确认映射真的生效
下面的命令按 Docker 官方文档整理,还没有在本站维护者的机器上运行过;“预期”一栏是根据文档推导的。你在自己的 Linux 机器上跑完,输出以你的为准。
先确认 8080 没被别的程序占用,也没有同名容器 lp-port-web。然后启动一个只供本机访问的 nginx:
这三条命令各回答一个不同的问题,单看任何一条都可能误判:
| 证据 | 回答的问题 | 预期(未实测) | 它证明不了什么 |
|---|---|---|---|
docker port | Docker 记下了什么规则? | 80/tcp -> 127.0.0.1:8080 | 规则存在,不代表程序在跑 |
ss -lntp | 宿主机上有没有进程在监听 8080? | 可能看到 docker-proxy,也可能什么都没有 | 没有条目不代表映射失败,内核 NAT 不需要监听进程 |
curl | 从这里发一次真实请求,能不能拿到页面? | nginx 默认页面的开头几行 HTML | 本机能通,不代表别的机器也能通 |
docker port 的输出格式可以对照官方文档里的示例。这是文档中名为 test 的容器,端口和本实验不同:[6]
注意方向: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 为空只说明没有用户态代理在监听,和内核转发不矛盾;而外部能否访问,这次实验根本没测。
决定谁能进来
绑定地址决定了哪些机器能用这个入口
-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 会怎样?再切换下面的选项核对。
-p 127.0.0.1:8080:80:只有宿主机自己能通过回环地址进来;另一台电脑访问 192.168.1.20:8080 时,这个地址上没有入口。
文字对照:绑定 127.0.0.1 只打开回环入口;绑定 0.0.0.0 打开所有 IPv4 地址上的入口。两种情况下转发目标都是容器的 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 宿主机的局域网地址。
预期:回环绑定时,第二台机器的请求超时或被拒绝;改成所有地址后,若网络策略允许,应能拿到 nginx 页面。做完记得 docker stop lp-port-web 关掉入口。
版本提醒:Docker Engine 28.0.0 之前,同一二层网络里的其他主机曾能访问到回环绑定的端口。本页结论只适用于 28.0.0 及之后的版本。[9]
带走可操作的判断
访问不通时,沿着包走的路逐段排查
按图 3 的顺序,从客户端一路查到程序。每一步都有一条命令能给出证据:
- 客户端访问的地址对吗?必须是 Docker 所在宿主机的地址和冒号左边的端口。另一台机器上的
localhost指向的是它自己。 - 规则在吗?
docker port lp-port-web里应该有你要的地址和端口。只写了EXPOSE不算发布。 - 入口开对地址了吗?本机能通、别的机器不通,先看是不是只绑定了
127.0.0.1,再查外部路由和防火墙。 - 程序在对的端口上吗?容器还在运行吗?
docker logs lp-port-web看程序是否启动、是否监听在冒号右边那个容器端口上。
要记住的
-p [地址:]左:右:左边是宿主机上的入口,可以换;右边必须对准容器里程序监听的端口。- 请求进来时内核改写目的地址,回程时改写源地址;容器里的程序看到的是客户端的真实来源。
docker port、ss、curl分别回答“规则在不在、有没有进程监听、请求通不通”,要一起看。- 不写地址就是对所有地址开放;只给本机用,就明确写
127.0.0.1。
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 模式。图中地址均为示意。
- Docker:Publishing and exposing ports — 参数顺序、默认 TCP、发布与声明的区别。
- Docker CLI:docker container run —
--publish、EXPOSE、--rm与-d。 - Docker with iptables — 端口映射的 NAT 规则、转发与连接跟踪。
- Docker CLI:dockerd —
--userland-proxy的用途与默认值。 - Docker:Packet filtering and firewalls — iptables 与 nftables 后端、Docker 与 ufw。
- Docker CLI:docker container port — 命令语法与本页引用的示例输出。
- ss(8),iproute2 手册 —
-l -n -t -p选项与过滤表达式。 - Docker Desktop:Networking — 虚拟机与桌面端口转发层。
- Docker:Port publishing and mapping — 绑定地址、默认发布到所有地址、IPv6、28.0.0 之前的回环问题。
- Docker Hub:nginx 官方镜像 — 默认在 80 端口提供 HTTP 服务。
- curl 手册 —
--noproxy、-f -s -S、--connect-timeout。 - Docker Rootless:Networking errors — 无管理员权限模式下额外的网络层。
- Moby:docker-proxy 的 TCP 实现 — 接住前端连接、连接后端、双向转交数据。