如果你已经在用 Docker 跑服务,我猜你迟早会遇到这样的问题:两台容器明明就在同一个宿主机上,却互相 ping 不通;某个服务重启之后 IP 变了,配置里写死的地址全部失效;项目里容器越来越多,一时搞不清楚谁和谁通信走的哪条链路。这些现象散落在各个技术群里,最后都被统一归因为一句——Docker 网络不通。
这句话本身没有太大参考价值,因为“不通”的原因可能有一百种:网段冲突、防火墙拦截、NAT 规则被覆盖、网络模式用错,甚至只是容器名解析问题。作为系列第 8 篇,这篇文章想把 Docker 网络的全貌从头到尾捋清楚:从五种内置网络模式各自的工作方式,到 bridge 网络底层的 veth、iptables 机制,再到自定义网络怎么用、常见故障怎么查。看完之后,你面对“网络不通”至少能自己定位到具体环节,而不是盲目重装。
1. Docker 网络是容器世界里最容易“黑盒化”的一层
1.1 为什么所有容器怪象最后都指向网络
容器的隔离体系里有三件套:文件系统隔离、进程隔离、网络隔离。文件系统用镜像层实现,你一眼能看到容器里跑的是哪个版本;进程用 PID namespace 隔离,也相对直观。唯独网络是隐形的——你在宿主机上执行 ip addr,看不到容器的 eth0;你不进到容器内部,也不知道容器当前拿到的 IP 是多少、网关是谁。这个“看不见”就是大量问题的土壤。
比如最常见的 docker-compose 部署。项目里有 nginx 和 php-fpm 两个容器,nginx 要通过 fastcgi 连 php-fpm。很多人的第一反应是在配置里写 127.0.0.1:9000,但容器里的 127.0.0.1 是自己的 loopback,根本到不了 php-fpm。这时候症状表现为“nginx 报 502”,看起来像是 php-fpm 没启动,折腾半天,实际是网络拓扑没理解。这种例子太多了,所以理解 Docker 网络不是可选项,是避坑必选项。
1.2 容器网络的本质:网络命名空间的隔离与连通
Linux 内核给网络提供了 namespace 级别的隔离机制。每个容器被创建时,Docker Daemon 会为它分配一个独立的网络命名空间,里面拥有一张自己的网卡、一套路由表,以及独立的 iptables 规则。容器以为自己活在独立的服务器里,其实它只有一张虚拟网卡和半份路由规则。
Docker 网络的工作,本质上是解决两个问题:隔离和连通。隔离靠 namespace 实现,连通则需要把不同 namespace 里的网卡想办法连起来。这个“连起来”的手法,正是 Docker 内置网络模式的核心区别。理解这一点,后面所有模式都变得顺理成章:bridge 模式是在宿主机上架一座虚拟网桥,host 模式是直接共用宿主机网络栈,container 模式是两个容器共用同一个网络栈,overlay 模式则是用隧道把两台物理机上的容器网络虚拟地拼在一起。
用生活化一点的说法:每个容器是独立房间,Docker 网络决定了房间之间用走廊、共用大门、还是干脆打通墙壁。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 五种内置网络模式:各自的机制、边界与适用场景
2.1 bridge 网络:默认模式下的 NAT 世界
Docker 安装后会自动创建三个内置网络:bridge、host、none。如果创建容器时不指定 --network,容器就会进默认的 bridge 网络。默认 bridge 对应 Linux 网桥设备 docker0,网段通常是 172.17.0.0/16,网关是 172.17.0.1。
在这个模式下,容器会拿到一个 172.17.x.x 的独立 IP,同一宿主机上的容器之间可以互相通信——数据包从容器 A 的 eth0 出发,经过 veth 对到达 docker0,再做三层转发到容器 B。容器与容器之间的通信不需要 NAT,也不需要走到宿主机外网口,这速度在单机场景下完全够用。
容器访问外网时就不一样了,数据包最终要从宿主机的物理网卡出去。Docker 会在 iptables 的 nat 表里加一条 MASQUERADE 规则,把容器网段的源 IP 替换成宿主机出口 IP,这就是经典的 NAT 行为。
但是默认 bridge 有几个明显的坑:它不提供基于容器名的自动 DNS 解析,容器之间互访靠写 IP 很容易因为重启而失效;它无法在容器运行期间动态接入网络;它的 iptables 规则非常宽松,默认允许所有容器互相访问。所以我的建议很直接:默认 bridge 只适合拿来跑单个测试容器,凡是要多容器协作的场景,都应该用自定义网络,这一点后面详细说。
2.2 host、none、container:三种特殊模式的取舍
host 模式:容器直接复用宿主机的网络命名空间,容器里看到的 IP 就是宿主机的 IP,没有独立 IP,没有 veth pair,也没有网桥转发。优点是零网络开销,性能等同宿主机原生进程;缺点是端口直接占宿主机,隔离性基本为零,容器里起 80 端口,宿主机其他服务马上受影响。-p 参数在 host 模式下不会生效,Docker 启动时会明确给出警告。
host 模式适合两类场景:一类是网络性能要求极高的应用,比如高性能代理、网络观测工具;另一类是端口数量特别多、不方便逐个映射的服务。除此之外,我更推荐用 bridge 加端口映射换一份可控性。
none 模式:容器只有 loopback 接口,彻底断网。这个模式看起来没什么用,但做安全隔离和纯离线计算时特别好使。比如跑一个需要严格与外界隔离的批处理容器,或者审计类工具,none 模式能保证没有任何意外出网路径。
container 模式:两个容器共享同一个网络命名空间,比如 --network container:nginx 会让新容器完全复用 nginx 容器的 IP、端口和路由表。这个模式在 sidecar 场景里非常实用:让日志采集容器和主业务容器共享网络栈,日志采集容器直接用 127.0.0.1:端口 就能抓主服务的流量,不暴露任何额外端口。代理容器、网络抓包容器也适合这种方式。
下面是五种模式的快速对照表,方便你记忆:
| 模式 | 容器独立 IP | 容器间通信 | 外网访问 | 典型场景 |
|---|---|---|---|---|
| bridge | 有 | 通过网桥 | 靠 NAT | 单机多容器最常用 |
| host | 无 | 共享宿主机 | 直接 | 性能敏感、端口多 |
| none | 无 | 不可通信 | 不可访问 | 安全隔离、离线任务 |
| container | 共享指定容器 | 共享网络栈 | 跟随共享容器 | sidecar、代理容器 |
| overlay | 有 | 跨主机隧道 | 经网关 | 多机集群通信 |
2.3 overlay 网络:跨主机通信的正规军
前面说的模式都只能在单台宿主机内工作。一旦 Docker 跑在多台机器上,容器跨宿主机互访就成了刚需。overlay 网络就是官方给出的跨主机答案。
overlay 网络基于 VXLAN 隧道技术。上层 Docker 先把集群初始化成 Swarm 模式(docker swarm init),然后创建 network create -d overlay 网络,所有加入这个网络的容器,不管物理位置在哪台机器,IP 都是同一网段。数据包从容器出发,到宿主机后被 Docker 封装进 VXLAN 隧道,通过物理网络传到目标宿主机的 docker 网桥,再解封装交给目标容器。
VXLAN 的 50 字节左右封装头会带来轻微性能损耗,但在多数应用场景下感知不明显。overlay 网络现在是 Docker Swarm 服务编排的标配网络层。如果你用的是 Kubernetes,通常不会直接用 Docker 内置 overlay,而是由 K8s 的 CNI 插件(如 Calico、Flannel)接管容器网络,原理思路其实高度相似。
3. bridge 网络的运作机理:veth、docker0、iptables 与 docker-proxy
3.1 veth pair:把容器“插”进网桥的虚拟网线
一个容器接入 bridge 网络时,Docker Daemon 会在宿主机网络命名空间里创建一对 veth pair 虚拟网卡。veth pair 的特点是天生成对,一端收到数据另一端立刻能看到。Docker 把这一对里的一端命名为 vethxxxxxx,挂到 docker0 网桥上;另一端则直接放进容器的网络命名空间,作为容器的 eth0。
这个设计相当于给每个容器拉了一根“虚拟网线”,一头插容器的网卡,另一头插中央交换机 docker0。你在宿主机上执行 ip link,能看到一大堆 veth 开头的接口,数量基本等于运行中并接入 bridge 网络的容器数。有几次我排查网络问题,发现某个 veth 接口状态是 NO-CARRIER,基本就能判断对应的容器网络配置有问题,根本不用进容器。
3.2 出网链路:NAT 如何替容器“背锅”
容器要访问外网时,数据包路径是这样的:容器 eth0 → veth 对 → docker0 → 宿主机内核路由判断目标 IP 不在本网段 → 走默认路由通过物理网卡发出。在 POSTROUTING 链上,有一条 Docker 写好的 MASQUERADE 规则会把源 IP 改成宿主机出口 IP。
对应的规则长这样:
bash复制iptables -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
含义就是:来自容器网段的数据包,只要不是从 docker0 口出去,就把源地址换成宿主机当前的那个出口 IP。
这里有个大前提你要记住:容器网段内部互访不走 NAT。容器 A 访问容器 B,数据包从 A 的 eth0 出去到 docker0,docker0 直接转发给 B,源 IP 始终是 A 的 172.17.x.x,目标 IP 始终是 B 的 172.17.x.x。外部网络对这个网段无感知无关紧要,但容器之间互相看到的永远是真实的容器 IP。这也是为什么容器日志里看到的请求来源 IP 往往是另一个容器的 IP,而不是外网真实 IP——等请求到达宿主机反代层再做一次改写,才可能拿到真实源地址。
3.3 端口映射:DNAT 规则与 docker-proxy 的双通道
-p 8080:80 这个参数背后做了一堆事情。首先是 Docker 启动一个 docker-proxy 进程,在宿主机上监听 0.0.0.0:8080,把收到的 TCP 流量原样转发给容器 IP 的 80 端口。与此同步,Docker 还会在 iptables 的 nat 表 PREROUTING 链写一条 DNAT 规则,把发往宿主机 8080 端口的包直接改写目标为容器的 80 端口。
也就是说,数据到达宿主机 8080 端口时,有“用户态 docker-proxy 转发”和“内核态 DNAT 重定向”两条通道。在纯 Linux 服务器上,DNAT 已经足够;docker-proxy 的存在更多是兼容非 Linux 环境的网络模型。你在系统进程列表里看到一堆 docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080,不用觉得奇怪,那是端口映射的外场守卫。
映射语法比很多人想得更细,几个实用变体:
bash复制# 只绑定本机回环地址,外部不可访问
docker run -d -p 127.0.0.1:8080:80 nginx
# 指定 TCP 和 UDP 双协议
docker run -d -p 8080:80/tcp -p 8080:80/udp nginx
# 随机映射所有 EXPOSE 端口
docker run -d -P nginx
查映射关系别猜,用命令直接看:docker port <容器名>,输出格式是 80/tcp -> 0.0.0.0:8080。我在实际项目里看到过不少次容器内部服务监听了 127.0.0.1 导致端口映射“不生效”的案例,所以写了端口映射后一定要先 docker port 确认规则,再进容器确认服务监听的是 0.0.0.0。
4. 实操部分:从默认 bridge 迁移到自定义网络
4.1 自定义网络的创建与网段规划
自定义 bridge 网络和默认 bridge 在底层都是 Linux 网桥,但 Docker 给自定义网络加了一层内置 DNS 解析,还允许运行中的容器动态接入、断开,并支持固定 IP。这些能力把默认 bridge 的坑基本填平了。所以我的习惯是:凡是要长期用的多容器项目,全部拉到自定义网络里。
创建命令很简单:
bash复制docker network create \
--driver bridge \
--subnet 172.20.0.0/16 \
--gateway 172.20.0.1 \
mynet
网段规划这条要特别上心。很多人的翻车现场都是自定义网段和宿主机现有网段冲突。比如你公司内网恰好是 172.20.0.0/16,这时候 Docker 网段再选 172.20.x.x,容器访问宿主机网段里的某些服务器就会产生路由混乱。稳妥的做法是先看宿主机路由表:
bash复制ip route
确认没有重叠网段之后,再挑一个不常用的私有网段。如果你懒得动脑,172.18.0.0/16、172.19.0.0/16 这些 Docker 保留网段通常都比 172.17 安全。
4.2 容器接入多网络与固定 IP
创建了网络,运行容器时直接挂进去:
bash复制docker run -d --name web \
--network mynet \
nginx
如果容器已经运行,再补充网络连接也一样:
bash复制docker network connect mynet web
这个能力很关键。一个容器可以同时挂多个网络,比如 nginx 需要同时访问前端网关网络和数据库网络,但两个网络内部又各自隔离,那就可以让 nginx 同时接入 frontend 和 backend。同一个容器在每个网络里会拿到不同的 IP,路由表会依据目标网段自动选择对应网卡出去。
固定 IP 的配置方式如下:
bash复制docker run -d --name mysql \
--network mynet \
--ip 172.20.0.10 \
mysql:8.0
需要注意:--ip 指定固定 IP 只能用于自定义网络,默认 bridge 不支持。IP 必须落在网络定义的 subnet 范围内,而且容器重启后 IP 能保留,这是自定义网络相对默认 bridge 的一大优势。如果你之前写过很多用 IP 互访的容器配置,改成固定 IP 后至少不用再担心重启一次 IP 漂移的问题。
4.3 用容器名通信:DNS 解析的正确打开方式
自定义网络真正让人省心的地方在于 DNS。容器进入自定义网络后,Docker 内置 DNS(监听在容器的 127.0.0.11)会自动把容器名注册成 A 记录。于是容器 A 访问容器 B 直接写容器名就行:
bash复制docker run -d --name mysql --network mynet mysql:8.0
docker run -d --name app --network mynet myapp
# 在 app 容器里直接解析
docker exec app getent hosts mysql
返回的结果就是 mysql 容器的 IP。这样写出来的配置不怕 IP 变动,也不怕容器重建。
还能进一步用网络别名(alias)把服务名抽象出来。比如两台 MySQL 实例都可以注册同一个 alias db,应用层连接 db:3306 时,Docker DNS 会轮流解析,相当于在 DNS 层做了简单的负载均衡或主备切换:
bash复制docker run -d --name mysql-1 --network mynet --network-alias db mysql:8.0
docker run -d --name mysql-2 --network mynet --network-alias db mysql:8.0
这也是老教程里 --link 完全力不能及的能力。--link 本质是往 /etc/hosts 写死一条映射,容器重建后 IP 变了就失效,而自定义网络是动态 DNS 解析,容器重建后自动更新。
4.4 docker-compose 场景下的网络配置
docker-compose 项目默认会自动创建一个以项目名命名的 bridge 网络,同一个 compose 文件里的服务自动加入这个网络,服务名就可以作为 DNS 名互相访问。这套行为是我认为 compose 最顺手的部分,你在项目里写 db:3306、redis:6379,网络这一层不用操任何心。
但如果你的容器一部分用 docker-compose 管理,一部分用 docker run 起在外部网络里,两边默认互不可见。解决方案是在 compose 文件里声明使用一个外部已存在的网络:
yaml复制version: "3"
services:
app:
image: myapp
networks:
- mynet
networks:
mynet:
external: true
这样 compose 里的服务就能直接加入之前用 docker network create mynet 创建的网络,与 docker run 启动的其他容器共享 DNS 和连通性。这个技巧在“前端 compose 起,后端单容器调试”的本地开发流程里非常实用,省掉大量 --network-alias 的兜底操作。
5. 常见网络故障排查:从现象反推根因
5.1 排查前的三板斧
拿到任何“网络不通”的问题,我先做三件事:
- 看拓扑:
docker network inspect <网络名>,直接看这个网络下面挂了哪些容器、每个容器的 IP 是什么、容器是否运行。这个命令能回答“它们到底在不在一个网络里”这个最大前提。 - 进容器里看实际配置:容器里执行的
ip addr、route、cat /etc/resolv.conf可能是最直接的证据。如果不想docker exec进去,也可以拿到容器 PID 后用nsenter进入网络命名空间:
bash复制docker inspect -f '{{.State.Pid}}' <容器名>
nsenter -t <PID> -n ip addr
- 抓包确认流量走向:在宿主机上用
tcpdump看 docker0 或者指定 veth 接口上的流量,能立刻分辨数据包有没有到达网桥。
5.2 三类高频故障的排查链路
场景 A:容器 A ping 不通容器 B
这种问题八成是拓扑问题。排查顺序:
docker ps确认两个容器都在运行。docker network inspect <网络名>确认两个容器挂在同一个网络下。- 如果不在同一个网络,用
docker network connect把容器接入目标网络。 - 确定同网络后,进容器 A
ping <容器B的IP>,确认链路通。 - 链路通了但
ping <容器B的名字>失败,检查是否一个在自定义网络、一个在默认 bridge——默认 bridge 不提供容器名解析。 - 名字也通但应用端口不通,进容器 B 检查服务监听地址,确认不是只监听了 127.0.0.1。
场景 B:宿主机能访问容器映射端口,但局域网其他机器不行
这个场景通常不是容器的问题,而是宿主机网络策略:
docker port <容器>查看映射绑定的宿主 IP,如果是127.0.0.1:8080->80,那只有宿主机自己可以访问,属于绑定太严。- 确认宿主机防火墙状态。
firewall-cmd --list-ports或ufw status,放行对应端口。 - 云服务器还要检查安全组出/入方向规则。
- 再用
iptables -L FORWARD -n看 FORWARD 链有没有其他工具(比如 UFW 或 fail2ban)加出的 DROP 规则,把 Docker 容器转发的流量拦了。
场景 C:容器能 ping 通外网 IP,但解析不了域名
说明路由和 NAT 正常,卡在 DNS 上。
- 进入容器查看
cat /etc/resolv.conf,如果 nameserver 指向127.0.0.11,说明走的是 Docker 内置 DNS。 - 用
getent hosts <域名>试解析,不行的话检查宿主机 daemon.json 里的dns配置。 - 容器内临时改
/etc/resolv.conf不推荐,重启马上失效。正确的做法是:
bash复制docker run -d --dns 223.5.5.5 --dns 114.114.114.114 nginx
或者在 /etc/docker/daemon.json 里统一配置:
json复制{
"dns": ["223.5.5.5", "114.114.114.114"]
}
之后重启 Docker Daemon,所有新容器都会带上这两个 DNS。
5.3 提前预防:让网络问题不发生的几个习惯
踩过足够多的坑之后,我给自己定了几条近乎强迫症的习惯,全部是为了把问题消灭在发生之前:
- 凡是需要互相访问的容器,一律放进同一个自定义网络,绝不散落在默认 bridge 和 host 模式里混跑。
- 容器间通信一律写容器名加服务端口,不写死 IP。哪怕当前只有一台物理机,这个习惯也能保证未来迁移集群时零成本。
- 端口映射尽量精确绑定:明确 IP、明确端口、明确协议。能用
127.0.0.1:8080:80就不要用0.0.0.0:8080:80,减少暴露面。 - 起容器前先执行
docker network ls,看一眼现有网络,避免重复创建相同网段的网络自相残杀。 - 网络残留多了就清理:
docker network prune会把没有被容器引用的网络一并删掉,保持环境干净。
做排障这几年,我对 Docker 网络最大的体会是:先搞清楚模式,再谈排查。拿到一个“网络不通”的报障,我从来不急着进容器里 ping 来 ping 去,而是先执行 docker network inspect,把当前容器挂在哪个网络下、网段是什么、DNS 怎么配置,一次性看清楚。网络问题九成以上是拓扑问题,而不是链路问题,把拓扑弄明白了,剩下的就是按图索骥。希望这篇文章能把你从“遇事重装”里解脱出来。
