最近在排查一套服务的容器间通信问题,跟网络相关的几个坑连续踩了一遍,顺便把 Docker 网络这块彻底梳理了一次。如果你在部署容器时也遇到过“容器明明起来了但访问不了”“容器之间互相 ping 不通”“端口映射不生效”这类问题,那这篇内容应该能帮你少走不少弯路。
Docker 网络模式这件事,听起来是个基础话题,但实际用起来很容易混乱。默认安装完 Docker 之后,大家通常会直接docker run -p 8080:80这样跑起来,能访问就当网络没问题了。但一旦涉及多容器协作、跨主机部署、或者需要对网络做精细控制,就得把这些模式背后的逻辑搞清楚。这篇我会把 Docker 的几种网络模式、底层工作机制、配置细节和常见的坑全部讲清楚,顺便附上可以直接抄的排查命令。
1. 先搞明白 Docker 网络到底在解决什么问题
1.1 容器网络隔离的底层逻辑
要理解 Docker 网络,得先知道容器之间为什么需要“隔离”和“互通”并存。Docker 容器本质上是宿主机上的进程,但它用了 Linux 的 namespace 机制,让每个容器拥有自己独立的网络协议栈。这意味着每个容器可以有自己的 IP 地址、路由表、防火墙规则,就像一台独立的机器一样。
我打个方便理解的比方:Docker 的容器网络隔离,相当于在一栋大楼里给每个房间安装独立的电话线和门牌号。原本所有住户都在楼里共享一个总机,但现在每家可以有自己的号码,也能通过楼道互相打电话。这个“楼道”就是 Docker 的虚拟网桥,而“电话线”就是 veth 虚拟网线。
这套机制的核心组件有三个:network namespace(网络命名空间)、veth pair(虚拟以太网线对)和 bridge(网桥)。network namespace 提供了隔离边界,veth pair 是虚拟的交叉线缆,把容器的网络栈和宿主机的网桥连起来,bridge 则像一个虚拟交换机,负责转发二层数据帧。
用生活中的例子理解 veth pair:它就像一根双头的 USB 充电线,一头插在容器里(eth0),另一头插在网桥上(vethxxx),数据从一头进去,必然从另一头出来,两边严格一一对应。容器的所有进出流量都靠这根“虚拟网线”传输。
1.2 一张表看懂四种网络模式的本质区别
Docker 网络按隔离程度和资源利用方式,主要分为四种模式:
| 模式 | 网络隔离 | IP 管理方式 | 端口映射 | 适用场景 |
|---|---|---|---|---|
| bridge | 每个容器独立 IP,容器间可互通,与宿主隔离 | 由网桥自动分配 | 需要手动 -p 映射 | 最常见的默认模式,适合单机多容器通信 |
| host | 无隔离,直接使用宿主网络栈 | 直接使用宿主 IP | 不需要也不能映射 | 对网络性能要求极高、端口不冲突的场景 |
| none | 完全隔离,无任何网络接口 | 无 IP | 无法映射 | 高安全场景、只需本地回环的场景 |
| container | 与指定容器共享网络栈 | 共用同一 IP | 由共享的容器决定 | 两个容器需要极紧密协作时,如 sidecar 模式 |
每个模式的适用边界很清楚。bridge 是最省心的默认选择,host 牺牲隔离换性能,none 追求彻底的封闭,container 则是强行把两个容器的网络栈合并到一起。你需要在动手配置前就明确自己的优先级,否则后面改起来很麻烦。
1.3 Docker 网络驱动的扩展思路
除了上面四种内置模式,Docker 还支持外部网络驱动插件,比较常见的就是 overlay 和 macvlan。overlay 用于跨主机容器通信,会把多个宿主机上的容器放进同一个虚拟子网,逻辑上跟单机通信没有差别,适合 Swarm 或 Kubernetes 这类集群环境。macvlan 则让容器直接使用宿主所在物理网络的 IP,相当于把容器当成局域网里的一台独立物理设备,常用于需要容器 IP 与人网设备互通的场景。
我在实际项目中用 overlay 比较多,跨主机场景下它基本是标准答案。但需要注意:overlay 对网络质量有要求,VXLAN 隧道会带来一定的额外开销,如果在云上部署,还得保证数据平面的端口能互通。后续的章节我会重点展开这些配置细节。
2. Bridge 模式:最常用的默认网络配置
2.1 默认 bridge 网络的工作机制
启动 Docker 之后,宿主机上会自动创建一个名为 docker0 的虚拟网桥。这个网桥默认地址是 172.17.0.1/16,所有不指定网络参数的容器启动后,都会被挂到这个网桥上,并自动分配一个 172.17.0.0/16 网段内的 IP。
整个数据通路是这样的:容器 eth0 —— veth pair —— docker0 网桥 —— 宿主机路由表 —— 外部网络。当容器访问外网时,数据帧从容器 eth0 发出,通过 veth pair 到达 docker0,再经过宿主机的 NAT 规则做源地址转换,把源 IP 换成宿主机 IP 之后送入物理网口。这样,外部网络看到的始终是宿主机 IP,不会直接看到容器的内网 IP。
NAT 的规则是在创建默认 bridge 网络时自动写入 iptables 的,具体在 POSTROUTING 链里。我在排查问题时经常遇到一种情况:手动清过 iptables 规则、或者重启过防火墙,容器就突然无法访问外网了。原因就是 NAT 规则丢了,重建网络或者重启 Docker 服务后才能恢复。
2.2 容器之间如何互相访问
在同一个 bridge 网络中,容器间可以直接通过 IP 通信。Docker 在网桥上做二级转发,同一网桥下的容器互通不需要经过宿主机协议栈的 NAT,直接二层就能到。这也是为什么容器互访延迟低、吞吐高。
但只靠 IP 通信有个问题:容器重建后 IP 会变。比如你用docker run启动一个 A 容器,它的 IP 是 172.17.0.2,后来它挂了,你重新启动一个新的 A',IP 很可能变成 172.17.0.4,此时 B 容器里如果写死了 A 的 IP 来访问,就会全部走不通。
解决这个问题有两类办法。一类是使用自定义网络(下一章展开),另一类是使用--link参数在容器启动时建立连接关系。--link的原理是在目标容器的 /etc/hosts 里添加一条主机名到 IP 的映射,加上环境变量的注入。但它本质上是静态绑定,容器重启后 IP 变了,link 关系可能还是会断裂,所以现在官方已经把它标记为遗留功能,不推荐在新项目里使用。
2.3 Bridge 模式常用参数与端口映射
使用默认模式时,最常见的命令就是docker run -p来映射端口。-p 参数格式为-p <宿主机IP>:<宿主机端口>:<容器端口>/<协议>,比如:
docker run -d --name web -p 8080:80 nginx docker run -d --name web -p 127.0.0.1:8080:80/tcp nginx第一行表示将宿主机的 8080 端口映射到容器内的 80 端口,宿主机所有网卡上的 8080 都会被占用。第二行则只监听回环地址的 8080,外部机器无法访问,只能本机访问。后者在开发环境或本地调试时特别适合,可以避免不小心把服务暴露到网上。
需要特别注意的是:-p 的映射是依靠 iptables DNAT 规则实现的,它并不修改容器内的端口或 IP 配置。动手排查时,可以用iptables -t nat -L -n | grep 8080看看映射规则是否还在,判断映射失败的原因。
另外,如果你想给容器设固定 IP,可以这样:
docker run -d --name web --network bridge --ip 172.17.0.100 nginx但一定要设在与网桥同网段且没有被占用的地址上,否则启动会失败。默认 bridge 模式下这种固定 IP 的方式只对手动指定的 IP 有效,容器重建后该地址还是会被其他容器占用,所以生产环境有固定 IP 需求时用自定义网络更合适。
3. Host、None、Container 模式的使用场景
3.1 Host 模式:性能优先的取舍
Host 模式启动时加--network host,容器不再拥有独立的 network namespace,直接使用宿主机的网络协议栈。也就是说,容器内的进程监听的端口就是宿主机端口,不需要也不能再做端口映射。
我第一次用 host 模式是跑一个性能敏感的 UDP 应用。当时用 bridge + NAT,实测吞吐量损失在 10% 到 20% 之间,换成 host 模式后性能完全追平了裸进程。这个模式的优点就在这:没有 NAT 转换、没有额外的数据包转发层次,网络路径极短,延迟低、吞吐高。
缺点也很明显:隔离性完全消失。容器里跑的程序可以监听宿主机的任意端口,如果同时跑多个容器且端口未规划好,会直接冲突。另外,host 模式下容器内的网络配置与宿主机完全一致,想用自定义 IP 是不现实的,也没有办法用 docker0 网桥做网络策略限制。
适用场景大概有这么几类:高性能 UDP/TCP 网关、需要直接使用宿主网络接口做抓包分析、对端口动态绑定要求高的场景。如果你的服务不是性能敏感型,我还是建议老老实实用 bridge,毕竟隔离性带来的安全收益通常比那点性能更重要。
3.2 None 模式:彻底隔离的封闭网络
None 模式是最“干净”的网络模式,容器内只有回环接口 lo,没有 eth0,没有 IP,也没有任何外部通信能力。适用场景主要有两类:一类是强调安全隔离的离线计算任务,容器内部完成全部逻辑,连网也不给它,从根本上杜绝网络层攻击;另一类是运行一些只需要本地回环通信的程序,比如本地测试专用工具。
有人会问,完全没网的话容器怎么更新代码?常规做法是挂载数据卷进去,代码和数据通过卷共享,不让容器主动访问网络。如果容器需要某些依赖,但又不想让它连外网,可以在启动前用 Dockerfile 把依赖包装进镜像里,运行时完全离线。
None 模式的配置也很简单:
docker run -d --name isolated --network none nginx跑起来之后进入容器执行ip addr,你会发现只有一个 lo。这在实际调试安全问题或者做隔离实验时非常好用,可以帮你确认程序在“完全没有网络”的环境下是否会因为意外尝试连接而超时或崩溃。
3.3 Container 模式:共享网络的协作容器
Container 模式用--network container:<容器名或ID>指定,让新容器与目标容器共享同一个网络栈。此时新容器没有自己的 eth0 和 IP,而是直接拿着目标容器的 IP 和网络配置来用。
这个模式下两个容器就像同一个虚拟机里的两个进程,通过回环地址 lo 就能互相通信。典型用法是 sidecar 代理:主容器监听一个端口,旁边的辅助容器通过127.0.0.1:port连到主容器,代理或辅助服务不需要对外暴露额外端口。
Container 模式有个很重要的特性:网络共享是一体的,端口监听和防火墙规则也只存在一份。你没法给其中的一个容器单独映射端口,要映射就必须在同一网络栈内去配置。另外要留意生命周期问题——被共享网络栈的目标容器如果停止或被删除,另一个容器也会立刻失去网络能力,影响范围比较大。
如果你的服务不需要这种紧耦合的共享网络栈,能不用 container 模式就不用,因为它的排错逻辑更绕,很多问题跟网络配置耦合在一起,排查起来费用不低。
4. 自定义网络与生产环境网络选型
4.1 为什么默认 bridge 不够用
默认 bridge 网络在单机、小规模场景下挺顺手,但一涉及更复杂的需求就露短板了:
- 默认 bridge 不启用 Docker 内嵌的 DNS 解析功能。容器只能用 IP 访问彼此,想用容器名解析不行,除非手动加
--link或者自己维护 /etc/hosts。 - 默认 bridge 的 IP 池是固定的,多个项目共享同一个 docker0,容易造成网段冲突。在有多个 Docker 实例的宿主机上,手动规划网段会非常痛苦。
- 默认 bridge 下所有容器之间默认互通,没有隔离粒度,想限制个别容器的访问还得自己写 iptables 规则。
自定义网络就是为解决这些问题而存在的。Docker 允许你创建一个新的 bridge 网络,可以指定子网、网关、DNS 解析等,容器加入后自动获得该网络的 DNS 解析服务。
4.2 创建自定义 bridge 网络的完整配置
创建自定义网络的命令格式,先看参数列表:
docker network create \ --driver bridge \ --subnet=192.168.88.0/24 \ --gateway=192.168.88.1 \ --ip-range=192.168.88.128/25 \ --opt com.docker.network.bridge.name=my-bridge \ mynet这里每个参数都不是随便拍的。subnet 表示网络子网范围,gateway 表示该网络的网关地址,ip-range 表示 DHCP 分配容器的地址范围(相当于限制可自动分配的 IP 池),--opt bridge.name则把宿主侧网桥名自定义为 my-bridge,方便抓包观察。
注意:如果你指定了 subnet 却没指定 gateway,Docker 默认取 subnet 的第一个可用地址作为网关。如果你指定了 ip-range,那这个范围必须位于 subnet 之内,否则创建会失败,这些细节先想清楚再执行。
创建好后启动容器,指定--network mynet,就能直接看到容器获得了该子网段内的 IP。接下来在同一网络内的容器间可以用容器名互相访问,Docker 内置的 DNS 服务器会自动把容器名解析成对应 IP。这个特性非常实用,容器之间不需要在乎彼此的 IP 是否变化,直接写服务名即可。
测试方式:
docker run -d --name app1 --network mynet redis:7 docker run -d --name app2 --network mynet --network-alias cache redis:7 docker exec app1 ping cache上面的--network-alias cache表示给 app2 起了一个网络别名,同一网络的其他容器能用cache这个别名访问它。这个机制在服务迁移或滚动更新时非常有用。
4.3 跨主机网络:overlay 与 macvlan 的配置要点
单机的自定义 bridge 处理不了跨主机问题。比如两台机器上都跑着容器,容器 A 在机器甲,容器 B 在机器乙,它们不能直接通过彼此的容器 IP 访问,因为各自宿主机上的 docker0 网段是相互隔离的,也不在同一个二层环境里。
跨主机方案里最主流的是 overlay 网络。它在每个宿主机上运行一个 VXLAN 隧道端点,把跨主机的容器网络包封装在宿主机网络里,形成一种虚拟的分布式交换机。配置方式:
docker swarm init # 初始化集群 docker network create -d overlay --attachable my-overlay docker service create --name demo --network my-overlay --replicas 3 nginx这里有个细节:如果是给独立容器(非 service)用 overlay 网络,必须加上--attachable参数,否则普通容器无法加入这个网络。我在做混合部署时被这个问题卡过一次,不加这个参数,容器一直报网络无法加入。
用 overlay 网络时,容器间的通信路径变长,吞吐性能有损耗。如果公司内网时延低、带宽足,实际业务上影响不大;但如果跨地域部署,VXLAN 开销会让延迟明显上升,此时可以考虑使用 macvlan 直接把容器挂到物理网络里。
macvlan 的创建方式:
docker network create -d macvlan \ --subnet=192.168.1.0/24 --gateway=192.168.1.1 \ -o parent=eth0 mymacvlan容器接入后,它会获得一个物理局域网内可路由的 IP,外部设备可以直接访问它,不需要端口映射。macvlan 的坑也不小:它要求底层网络设备支持混杂模式(VPS、云环境里一般都不支持),它会跟宿主机的物理网卡地址共享一个 MAC 地址池,某些交换机会限制一个端口上的 MAC 地址数量,实际用之前先在测试网络里验证一下。
4.4 端口映射的细节与坑
端口映射是日常用得最多、踩坑也最多的部分。-p的完整格式再梳理一次:
-p <宿主端口>:<容器端口> -p <宿主IP>:<宿主端口>:<容器端口> -p <宿主IP>::<容器端口> # 宿主端口随机 -p <宿主端口>:<容器端口>/udp我比较推荐在生产环境指定宿主 IP,避免服务暴露到所有网卡上。尤其是机器有多张网卡或绑定公网 IP 时,如果不加宿主 IP,等于把所有接口上的端口都对外开放了,安全风险比较大。可以先只监听回环地址,再用反向代理统一转发出去,这样更可控。
另外,容器里如果程序还没起来监听端口,即使 -p 配好了,外部也访问不了。排查这类问题时先到容器内确认:docker exec <容器名> netstat -tlnp,看看目标端口有没有真的被监听。
5. 常见网络故障与排查技巧实录
5.1 容器访问不了外网的排查思路
遇到容器无法访问外网,按这个顺序排查,基本都能找到根因。
首先确认容器能不能 ping 通网关,比如默认 bridge 的网关 172.17.0.1。ping 不通,可能是网桥或路由表出问题,去宿主机上看ip route有没有到该网段的路由。网关通了但外网不通,重点检查 iptables 的 NAT 规则:iptables -t nat -L POSTROUTING -n -v,看看有没有 MASQUERADE 规则。如果 NAT 规则被防火墙重置清掉了,重启 Docker 服务会自动重建,但重启后正在运行的容器不会自动恢复网络,需要重启容器才能重新加入网络。
第二个常见点是 DNS 解析问题。容器里可能默认配置了 8.8.8.8 或 114.114.114.114,但宿主环境里可能屏蔽了外部 DNS 或者有内网 DNS 要求。遇到域名解析失败时,可以临时指定 DNS 启动测试:
docker run -d --name test --dns 10.0.0.2 busybox sleep 3600注意:如果宿主机开启了防火墙,并且默认规则是 DROP,那么即使容器网络配置正确,出去的包也可能被防火墙拦截。需要检查 FORWARD 和 OUTPUT 链里的策略,确保有允许容器网段转发的规则。
5.2 容器间互相 ping 不通
如果同一个 bridge 网络里的两个容器 ping 不通,多半不是网络模式的问题。第一步确认两个容器是不是真的在同一个网络里,否则隔离级别不同,根本不互通。这很正常:不同自定义网络之间默认隔离,互相访问需要有额外的路由或共享网络配置。
同一个网络里也不通的,看两种情况。一种是容器分配 IP 冲突,特别是手动指定 IP 时最容易发生。排查:在宿主机上ip neighbor查看网段内各 IP 对应的 MAC,再对照容器的 MAC,看看是否有多余条目。另一种是容器内没有默认路由,虽然容器有 IP,但没设置网关。检查容器内的ip route,正常情况下至少有一条到网关的默认路由。
踩过一次很典型的坑:容器里跑的进程依赖双栈(IPv4/IPv6),但 Docker 网络没有分配 IPv6 地址,进程尝试用 IPv6 连接一个不存在的地质,导致等待时间很长才超时。这种问题特征很明显,行为上表现为“能通但特别慢”,直接用docker exec <名> ip -6 addr看看容器内 IPv6 的状态,确认有无地址再配置。
5.3 手动改 iptables 导致 Docker 网络故障
这个坑我在生产环境里碰到过两次。系统里可能有其他程序(例如防火墙管理脚本、云平台 agent)定期重置 iptables,每次重置后,Docker 写入的 NAT 规则就会丢失。表现是:所有容器还能看到 IP,但端口映射全部失效,容器访问外网也断开。
修复方式并不复杂:systemctl restart docker,重启后 Docker 会重建网络和 iptables 规则。但这会让所有容器重启,影响服务连续性。更稳妥的做法是搞清楚是谁在重置规则,在防火墙配置里放行 Docker 相关的链和规则,或者在启动脚本里把 Docker 网络需要的规则重新刷上去。
推荐一个更干净的方式:不用 Docker 默认的 iptables 管理机制,在 daemon.json 里配置:
{ "iptables": false }这样 Docker 不再管理 iptables 规则,你需要自己维护 NAT、过滤规则。适合网络策略严格受控的环境,但千万别在没把握的情况下直接设置,因为这意味着你把端口映射和 NAT 的职责全部自己承担了,配置失误会导致容器完全无法对外通信。
5.4 容器重建后 IP 变化带来的连带问题
容器重建导致 IP 变化,在自定义网络下可以用容器名访问来解决。但有些场景,比如外部系统需要通过固定 IP 访问容器,就需要给容器指定固定 IP。
自定义网络下可以这样:
docker network create --subnet=10.10.0.0/16 static-net docker run -d --name fixed-ip \ --network static-net --ip 10.10.0.50 nginx这里的注意点:--ip指定的地址必须在网络的 subnet 范围内,而且不能用网关地址,同时得避开网络中已经占用的 IP。如果容器要支持动态调整 IP,最好配合 Orchestrator(如 Swarm 或 K8s)来做,而不是依赖 Docker 本身的参数组合。
另一个隐藏问题:如果容器删了但网络里还有 DNS 缓存,其他容器用旧的容器名去解析,有可能会解析到已删除容器的 IP一段时间。遇到这种情况不花时间等缓存过期,直接重启一下依赖方容器,比在那儿猜半天更有效率。
5.5 常见问题速查表
下面把平时遇到最多的几个问题整理成一张速查表,直接在排障时对照处理:
| 故障现象 | 可能原因 | 排查与修复 |
|---|---|---|
| 容器无法 ping 通外部 IP | 网关不通或 NAT 规则丢失 | 检查宿主机路由表和 iptables NAT 链,重启 Docker 重建规则 |
| 域名能通但 IP 不通 | 容器 DNS 配置失效 | 检查 /etc/resolv.conf,用--dns指定正确 DNS |
| 端口映射外部访问不到 | DNAT 规则被清或服务未监听 | 确认容器进程已监听端口,检查 iptables nat 规则 |
| 容器间互 ping 不通 | 不在同一网络或 IP 冲突 | 确认网络类型一致,查看ip neighbor是否有冲突 |
| 容器 IP 每次重启都变 | 默认动态分配机制 | 使用自定义网络并固定--ip,或改用容器名访问 |
| 跨主机容器互通失败 | overlay 端口被防火墙拦截 | 确认 UDP 4789 等端口放行,尝试关闭防火墙再看现象 |
| Container 模式下目标容器停止 | 新容器网络中断 | 重启或替换目标容器,避免依赖方长时间离线 |
排查网络故障的逻辑链其实很固定:先分清楚网络模式,再确认宿主机网络是否正常,接着看容器内网络配置,最后看 iptables。按照这个顺序来,绝大多数问题都能迅速定位。
6. 个人踩坑记录与配置心得
6.1 一个典型的跨主机通信隐患排查案例
曾经维护过一套模拟项目,用 Docker Swarm 部署了多个服务,其中 A 服务和 B 服务分别跑在不同节点上。某次升级后,A 服务突然无法访问 B 服务的 API,报错超时。排查的第一步先看了两边的容器网络确认都加入了同一个 overlay 网络,然后检查跨节点连通性。
当时用docker service ps查看任务分布,发现 A 和 B 的服务实例分散在三台节点上,但有两个节点之间的 UDP 4789 端口被防火墙拦截了。VXLAN 隧道的数据包就是走这个端口进行封装的,端口不通,overlay 网络自然就断了。当时调整防火墙规则后,容器通信立刻恢复。这类典型的问题在于,团队里很少有人会专门记住 overlay 网络依赖的端口,修改防火墙时只会放行 HTTP 和 HTTPS,很少想到放行 UDP 数据面。
6.2 网络模式选择建议
根据我的经验,做个简单的决策建议:
- 单机部署几个容器,互相通信不频繁:直接用默认 bridge,简单省事。
- 单机但不希望容器之间 IP 写死、或者需要服务名解析:立刻换成自定义 bridge 网络。
- 性能敏感的 UDP 网关、抓包工具、端口动态绑定的程序:用 host 模式。
- 离线任务、计算隔离、完全不联网的容器:用 none 模式。
- 紧耦合辅助容器,如日志采集、边车代理:用 container 模式共享主容器网络。
- 跨主机多节点容器集群:优先考虑 overlay,若强依赖固定 IP 且物理网络支持混杂模式,再评估 macvlan。
配置网络前先在测试环境画出访问关系图,标清楚哪些服务需要互通、哪些需要暴露到外部、哪些互相之间必须隔离。图画清楚再选模式,比直接在命令行里反复试错强得多。
6.3 最后再分享一个小技巧
用 Docker 网络时,docker network inspect是你最好用的调试工具。它能看到网络里所有端点的详细信息,包括 IP、MAC、container ID、以及所属宿主机的 link 信息。遇到任何网络问题,第一反应就是执行:
docker network inspect <网络名> docker inspect <容器名> --format '{{.NetworkSettings.Networks}}'把这两条命令的输出对照起来看,容器到底有没有正确接入网络一目了然,比在宿主机上盲目抓包高效得多。我之前排查过一次容器 IP 冲突问题,就是通过 inspect 看到两个容器被分配了同一个 IP,才发现的端倪。Docker 网络的坑大多都有迹可循,掌握这套排查思路,你在实际部署时会顺手很多。