经常有同事把容器跑起来以后,网络一不通就来找我。问得最多的不是某个命令怎么写,而是一堆困惑:为什么两个容器在同一个 docker network 里能互相 ping 通,换成默认 bridge 就不通了?为什么容器里看到的 IP 和宿主机对不上?为什么我用了自定义网络驱动做“独立IP”,宿主机反而访问不了容器?这些问题的答案,最后都会落到三件事上:隔离网络、路由表、网络驱动。
这篇文章不是 Docker 命令速查,算是我把这两年排查容器网络问题时的思路和踩过的坑整理了一遍。默认 bridge 怎么用、自定义 bridge 怎么建、路由表怎么读、macvlan 这类自定义网络驱动什么时候能用什么时候会踩雷,下面按我实际操作的顺序讲。看得懂这篇,至少以后遇到“容器之间不通”“端口映射没反应”“容器上不了外网”这类问题,能自己用命令定位到具体环节,而不是把 Docker 整个重启一遍。
1. 为什么Docker网络这么难懂:先从三个基础概念说起
1.1 容器看到的网卡和宿主机看到的网卡
很多人以为容器里的 eth0 就是宿主机上的 eth0,这是最大的误解来源。Docker 在创建容器时,会为每个容器建一个独立的网络命名空间,再通过一对 veth 虚拟网线把容器“插”到虚拟交换机上。
我在排查时最常用的验证命令就两条:
docker exec app1 ip addr ip addr第一条看容器内部的网络接口,第二条看宿主机的网络接口。你会发现容器里 eth0 的 MAC 地址和宿主机 eth0 完全不同,它的 IP 也往往落在 Docker 自己划分的子网里。这对 veth 其实可以理解成一根网线的两个水晶头:一头插在容器里,另一头插在 Linux bridge 上。默认情况下,这个 bridge 叫 docker0,IP 通常是 172.17.0.1;如果你创建了自定义 bridge,它会变成另一个网桥,IP 可能是 172.28.0.1 这类地址。
明白这条,你就知道“容器隔离网络”本质上不是把容器关进真空,而是让容器走不同虚拟交换机,再通过 Linux 内核的转发和 NAT 规则决定谁能访问谁。隔离不是 Docker 发明的黑魔法,它靠的就是网络命名空间、veth、bridge 这一套 Linux 网络技术。
1.2 路由表决定“下一步往哪走”
容器内的路由表和宿主机一样,是一张“下一步往哪走”的决策表。我经常在容器里执行:
docker exec app1 ip route输出大致是这样:
default via 172.28.0.1 dev eth0 172.28.0.0/16 dev eth0 proto kernel scope link src 172.28.0.2这两行信息量很大。第一行是默认路由,意思是所有目标地址不在已知子网里的包,都交给网关 172.28.0.1 去处理。第二行是直连路由,表示 172.28.0.0/16 这个网段就在 eth0 同一侧,不需要过网关,直接靠 ARP 找到目标机器。
排查网络问题时,我一般先问:容器里有没有默认路由?如果有,网关能不能 ping 通?网关能通但外网不通,问题多半在宿主机转发或 NAT;如果连默认路由都没有,那大概率是网络类型选错了,比如用了 none 驱动,或者创建网络时漏配了网关。
1.3 网络驱动是连接方式的“模板”
Docker 把不同的连接方式抽象成“驱动”,你在docker network create时用--driver指定,不传默认就是 bridge。这里是我常用驱动的一个对照表:
| 驱动 | 适用场景 | 需要注意的点 |
|---|---|---|
| bridge | 单机容器隔离网络,最常见 | 自定义 bridge 才有内建 DNS,默认 bridge 建议少用 |
| host | 容器直接复用宿主机网络栈 | 没有独立 IP,端口直接占宿主机,适合性能敏感场景 |
| none | 不需要网络,纯跑任务 | 容器没有 eth0,只能靠docker exec或共享卷交互 |
| overlay | 跨主机容器通信 | 底层走 VXLAN,适合集群场景 |
| macvlan | 给容器分配物理局域网 IP | 宿主机无法直接访问容器,Wi-Fi 下慎用 |
| ipvlan | 共享宿主机 MAC 的局域网方案 | 对交换机 MAC 限制更友好 |
标题里的“自定义网络驱动”,我的理解分成两层:一层是上面这类内置驱动的自定义参数,另一层是 Docker 插件机制支持你装第三方驱动。日常用得最多、也是最容易踩坑的,还是 bridge 和 macvlan。接下来先从隔离网络实战讲起。
2. 最常用的隔离网络实战:从bridge到自定义bridge
2.1 默认bridge为什么不适合长期用
很多新手直接docker run不指定网络,容器全部挂在默认 docker0 上。这样确实能跑,但如果一个项目里有十几个容器,所有服务都在同一个 L2 网段里,没有任何业务层面的网络隔离。更麻烦的是,默认 bridge 不支持内建 DNS,容器之间互相访问只能用 IP,而容器重建后 IP 很容易变,维护成本直接拉满。
所以我做项目的第一件事,通常是创建一个专用 bridge 网络,把相关服务放进去,不相关服务放到另一个网络。这样就实现了第一层隔离:不同虚拟交换机上的容器,默认互相不可达。
创建一个自定义 bridge 网络,我通常会把子网和网关显式声明出来:
docker network create \ --driver bridge \ --subnet 172.28.0.0/16 \ --gateway 172.28.0.1 \ my-net有人问为什么要手动指定子网?因为 Docker 自动分配 IP 时有一定随机性,如果宿主机和多个网络之间已经有路由规则,一个不小心会让容器子网和现有网络冲突。手动固定子网之后,路由表、防火墙规则、监控告警都更好对齐,也方便后续排查。
2.2 创建隔离网络并接入容器
网络创建好之后,启动容器时用--network指过去:
docker run -d --name app1 --network my-net nginx:alpine docker run -d --name app2 --network my-net redis:7-alpine然后看网络里挂了多少容器:
docker network inspect my-net在Containers字段里能看到 app1 和 app2 各自的 IP,比如 172.28.0.2、172.28.0.3。这时候我会顺手做一次连通性验证:
docker exec app1 ping -c 2 app2如果通,说明自定义 bridge 的内建 DNS 把容器名解析成了对应 IP。这个特性非常关键,因为容器重建后 IP 可能变,但服务名是稳定的,代码里写服务名比写 IP 可靠得多。
再验证一下隔离性:另起一个容器放到别的新网络里,然后去 ping app1。你会发现网络不同时,即使 IP 段不冲突,默认也 ping 不通,因为数据包在宿主机这一层就被 DROP 了。这正是“隔离网络”最直接的效果。
2.3 用容器名做DNS解析
自定义 bridge 网络里自带一个 127.0.0.11 的 DNS 解析器。同一个网络内的容器,会自动把容器名注册成 DNS 记录。注意,默认 bridge 没有这个特性,只有通过--link老参数临时加别名,那个方案已经属于过期用法。
如果你希望同一服务对外暴露多个名字,可以在启动时加--network-alias:
docker run -d --name app1 --network my-net --network-alias api nginx:alpine这样 app2 里解析api也能找到 app1。这个技巧在拆分微服务、前后端联调时很好用,不用在容器里改/etc/hosts。
2.4 隔离性验证方法
我验证隔离性时会分三步走:第一步看容器路由表,确认两个容器确实在不同网段;第二步直接 ping 目标 IP,观察通断;第三步查宿主机iptables -L FORWARD -n -v,看 DROP 计数有没有增长。
还有一个保护性参数值得提:--internal。创建网络时加上它,网络内部容器之间仍然能通,但不会有默认路由出外网:
docker network create --driver bridge --internal \ --subnet 172.29.0.0/16 \ safe-net如果要做更严格的业务隔离,我一般会把数据库、消息队列这类后端服务放到--internal网络里,只有应用层容器放到能出公网的网络,两边通过 Docker DNS 和服务名解耦。这是我在实际项目中用得最多的隔离姿势。
3. 路由表里到底有什么:排查网络不通的第一步
3.1 进入容器看路由表的正确姿势
有一次线上容器访问另一个服务超时,开发在容器里curl半天没结果。我到他机器上做的第一件事就是看容器路由表:
docker inspect -f '{{.State.Pid}}' app1拿到进程 PID 之后,直接用宿主机上的 nsenter 进入容器的网络命名空间:
nsenter -t <PID> -n ip route这样不需要容器里装额外工具,只要宿主机有 iproute2 就能看。输出通常是这样:
default via 172.28.0.1 dev eth0 172.28.0.0/16 dev eth0 proto kernel scope link src 172.28.0.2如果目标服务在 172.28.0.0/16 网段内,容器会直接通过 eth0 找它,不经过网关;如果目标在网段外,比如公网地址,容器会先把包扔给 172.28.0.1,也就是自定义 bridge 的网关。这里的网关不是物理路由器,而是宿主机上的虚拟网桥。
你可能还会想看邻居表,也就是 ARP 缓存:
docker exec app1 ip neigh如果路由表正常但 ARP 表为空,说明容器连直连设备都没找到,大概率是 veth 配对或 bridge 上的端口出了问题。
3.2 宿主机路由表和iptables的配合
很多网络问题的根源不是容器缺路由,而是宿主机不愿意转发。Docker 创建网络时会自动打开 Linux 内核的 IPv4 转发,但如果你在云服务器上手动改过内核参数,这部分可能被关掉。
我排查外网不通的标准命令:
sysctl net.ipv4.ip_forward iptables -t nat -L -n -v | grep -E 'MASQUERADE|DNAT' iptables -L FORWARD -n -v容器访问外部网络的流程是这样的:容器发起请求,包从 veth 进到 bridge,宿主机根据路由表决定出口网卡,然后 NAT 规则会把源 IP 改写成宿主机 IP,这个动作就叫 MASQUERADE。你创建自定义 bridge 时如果特意把com.docker.network.bridge.enable_ip_masquerade设成了 false,容器的包出去后源 IP 不会伪装,外部网络如果没有回程路由,请求自然就断了。
端口映射也是同一个思路。你运行:
docker run -d -p 8080:80 --network my-net nginx:alpineDocker 不仅起了一个 docker-proxy,还在 iptables 里加了一条 DNAT 规则,把宿主机 8080 端口的流量转给容器 80 端口。宿主机防火墙如果禁掉了转发,或者你自己把 Docker 的 iptables 链清了,这个映射就会失效。
3.3 跨主机通信时的路由走向
单机自定义 bridge 只负责本机容器。如果你有两台机器,机器 A 的容器想访问机器 B 的容器,直接用 bridge 是行不通的,因为机器 B 的容器 IP 不在机器 A 的任何路由范围内。解决方案一般就两种:一是用 overlay 驱动,让 Docker 用 VXLAN 隧道封装跨主机流量;二是用 macvlan 这类 underlay 方案,让容器直接使用物理局域网 IP,由交换机完成路由。
用 overlay 时,容器内看到的依旧是 eth0 和 overlay 子网,但宿主机上会多出虚拟隧道接口,数据包会被 UDP 封装后送到对端宿主机。这时候你光看容器路由表不够,还要看宿主机到对端宿主机能不能通、UDP 4789 端口有没有被防火墙拦。用 macvlan 时则要反过来,容器相当于物理网络里的一个独立设备,路由表由外部路由器决定。
4. 自定义网络驱动:不是随便填个名字就行
4.1 为什么需要macvlan
大多数场景用 bridge 就够了,但有些业务很特殊,比如旧系统只认物理 IP,或者需要在容器里绑指定端口,让局域网其他机器直接访问容器,不想经过宿主机 NAT。这时候就可以考虑 macvlan。
macvlan 的原理是让宿主机物理网卡虚拟出多个带不同 MAC 地址的子接口,每个容器像一台直连交换机的主机。容器的 IP 直接属于物理局域网网段,从外部看它就是一台普通设备。
创建 macvlan 网络时,我一般这样写:
docker network create \ -d macvlan \ --subnet 192.168.1.0/24 \ --gateway 192.168.1.1 \ -o parent=eth0 \ macvlan-lan然后运行容器并指定 IP:
docker run -it --rm \ --network macvlan-lan \ --ip 192.168.1.100 \ alpine sh容器启动后,ip addr里能看到 eth0 的 IP 是 192.168.1.100,它跟普通局域网主机处在一个网段,可以直接 ping 网关,也能被局域网其他机器访问。这种方式最接近“把容器当虚拟机用”的体验。
如果想在 macvlan 上划分 VLAN,也就是把“网络域隔离”下沉到物理交换机层面,可以预先建好 VLAN 子接口,再用子接口作为 parent:
ip link add link eth0 name eth0.100 type vlan id 100 ip link set eth0.100 up docker network create \ -d macvlan \ --subnet 192.168.100.0/24 \ --gateway 192.168.100.1 \ -o parent=eth0.100 \ macvlan-vlan100这样一个物理口拆出多个二层网络,再在交换机侧做 ACL 控制,就能把不同业务的广播域彻底隔离。需要特别提醒的是,VLAN ID 需要宿主机到交换机之间是 Trunk 口,否则包会被交换机丢掉。
4.2 macvlan的三大坑点
坑一:宿主机无法直接访问 macvlan 容器。这个现象我第一次遇到时也很懵,网络类工具用ping 192.168.1.100完全不通,但从另一台物理机能通。原因在于 macvlan 的 bridge 模式不会让宿主机和自己的子接口通信。如果确实需要宿主机访问容器,可以用 ipvlan 代替,或者在宿主机上再加一个同网段的 macvlan 接口,专门承担访问流量。
坑二:Wi-Fi 场景基本别用。家用路由器开了 AP 隔离,或者无线网卡本身不转发陌生 MAC 地址时,macvlan 容器会频繁“失联”。我个人的经验是,只要宿主机是笔记本 WLAN,直接放弃 macvlan 方案,老老实实 bridge + 端口映射。
坑三:macvlan 驱动本身不做 DHCP。你以为容器能像普通设备一样自动从路由器拿 IP,但实际上 Docker 的 IPAM 默认是静态分配。要么用--ip指定,要么在创建网络时用--ip-range限定一段可分配地址。如果网络里还有别的 DHCP 设备,要小心 IP 冲突,最好把容器用的 IP 段在路由器上排除掉。
4.3 网络驱动扩展:从内置到插件
除了内置驱动,Docker 还支持第三方网络驱动插件。使用时照样是docker network create -d <插件名> ...,只是底层实现变成了插件进程。要想开发一个真正的自定义网络驱动,需要实现 Docker 插件规范里的 NetworkDriver 接口,核心方法包括网络创建、网络删除、端点加入、端点离开等。这个工作量不小,而且容器运行时对性能和稳定性要求很高。如果你只是想要一个特定形态的网络,我建议先从内置驱动参数下手,实在不够再考虑装开源插件或自研。
从经验来看,90% 的“自定义网络”需求,用自定义 bridge + 合理的 subnet 规划就能解决,macvlan 主要用于那 10% 需要容器“完全透明”接入物理网络的场景。上来就 macvlan 反而容易把简单问题搞复杂。
5. 常见问题与排查技巧实录
5.1 一张速查表帮你少走弯路
我整理了一张日常最常遇到的 Docker 网络问题速查表,排查时优先按这个顺序确认:
| 现象 | 优先检查 | 常见原因 |
|---|---|---|
| 容器 ping 不通外网 | 容器路由表、宿主机 ip_forward、NAT 规则 | 网络设置过于严格,MASQUERADE 被禁用 |
| 两个容器互相 ping 不通 | 是否在同一个 network、iptables FORWARD 策略 | 容器挂在不同网络,或防火墙 DROP |
| 端口映射了但外部访问不了 | docker port、DNAT 规则、容器进程监听 | 绑定到了 127.0.0.1,或容器内服务只监听 IPv6 |
| 容器 IP 不稳定 | docker inspect、docker compose 配置 | 没有固定 IP,重建后变化 |
| 宿主机访问不了 macvlan 容器 | 接入方式 | macvlan 宿主机与子接口不互通 |
| 容器 DNS 解析失败 | /etc/resolv.conf、网络是否为自定义 bridge | 默认 bridge 没有内建 DNS |
5.2 一套我常用的排查套路
再分享一套具体排查步骤,按顺序执行,基本能定位 80% 的问题。
先确认容器在哪个网络、分到什么 IP:
docker network inspect my-net docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' app1再进容器看路由和默认网关:
docker exec app1 ip route docker exec app1 ping -c 2 172.28.0.1网关能通但外网不通,就看宿主机转发和 NAT:
sysctl net.ipv4.ip_forward iptables -t nat -L -n | grep MASQUERADE端口映射有问题,就看 Docker 生成的规则:
iptables -t nat -L -n -v | grep -E 'DNAT|8080'如果还定位不到,直接用 tcpdump 抓包,站在数据链路层看包到底有没有到容器:
docker exec app1 sh -c "tcpdump -ni eth0 icmp -c 10"注意,千万不要一上来就iptables -F或者重启 Docker。我见过太多次因为手滑清了 Docker 的 iptables 链,结果所有容器网络瞬间瘫痪的情况。Docker 会把自己的规则加在 DOCKER 链和 DOCKER-USER 链上,你要管理自定义防火墙规则,请优先使用 DOCKER-USER 链,不要直接动 DOCKER 链。
5.3 我的一点工程习惯
最后说一个我长期用的习惯:每个项目固定一张网络配置。docker compose 里我会显式声明 network,而不是靠 Docker 默认随机分配。
networks: app-net: driver: bridge ipam: config: - subnet: "172.30.0.0/24" gateway: "172.30.0.1"这样所有服务挂到同一个 app-net,互相用服务名解析,容器之间依赖关系一目了然。子网固定之后,以后加防火墙规则、做流量镜像、写监控告警,都能直接用网段匹配,不用再去网页后台翻 IP。很多“网络不通”的问题,其实是工程规范问题,不是 Linux 内核问题,把网络规划做清晰,能省掉大量查路由表的时间。