
前阵子帮某开发者排查微服务部署问题现象很典型A容器里的服务起来非常正常B容器里的客户端却始终连不上端口扫描是通的业务请求就是超时。查了一下午最后定位到根源不是代码而是两个容器被放在了不同的docker网络里。后来把网络模式统一到自定义bridge问题当场消失。类似的问题我见过太多次大部分人开始用Docker时对网络模式的认知基本停留在“-p能映射端口就行”直到容器一多、跨主机部署一上立刻被各种不通打乱节奏。这篇分享打算把docker网络模式及配置这件事一次说透从底层原理到命令实操再到真实踩过的坑按一个从业者的方式整体过一遍。适合刚开始接触Docker、想系统搞清楚网络的人也适合已经常用Docker但对网络细节把握不准的人。1. 先搞懂底层逻辑命名空间、veth和网桥1.1 网络命名空间每个容器一个“网络房间”Docker的隔离基础是Linux namespace其中网络命名空间network namespace决定了容器能看到哪些网卡、路由表、iptables规则和socket连接。每个容器默认都有独立的网络命名空间相当于每个人住一间独立的房间房间里只有自己的门窗网卡、门牌IP地址和装修路由规则。你在这个房间里改什么都影响不到隔壁房间。这条底层逻辑是所有网络模式的地基。理解它之后很多现象就不会再困惑了为什么宿主机上ifconfig看不到容器里的IP因为容器里的eth0在它的命名空间里宿主机看到的是一堆名叫vethxxx的虚拟网卡。排障的时候千万别在宿主机上满世界找容器IP直接docker inspect看比什么都快。1.2 veth pair一根跨房间的虚拟网线网络命名空间之间是不能直接互相访问的容器要和外网通信必须有一根“线”把房间接到走廊上。这根线就是veth pair。veth是一对虚拟网卡像一根虚拟网线一头塞进容器里另一头挂在宿主机网桥上。数据包从这头进去那头就出来反过来也一样。我在很多入门教程里都强调过你可以把veth pair理解成串口线两头必须成对出现光有一头是废的。Docker启动容器时自动创建一个veth pair一端变成容器里的eth0一端挂到bridge上。两个处于同一bridge网络的容器通信数据就是通过各自的veth进入bridge由bridge做二层转发。如果要访问外网则继续交给内核路由和iptables处理。1.3 网桥和NAT默认世界里的交换机与门卫Docker默认创建的那个bridge名字叫docker0。它本质上是一个虚拟二层网桥相当于一台傻瓜交换机。所有接入docker0的容器都在同一个二层网络中彼此可以直接广播和通信。docker0默认地址通常是172.17.0.1/16宿主机网关就是它。这里有个容易忽略的细节容器要上外网光靠bridge还不够。容器IP是私有地址出了宿主机就没法路由。Docker通过内核的IP转发和iptables的MASQUERADE规则把容器发出的数据源地址改成宿主机IP相当于门卫把每个人的门牌换成小区的地址。端口映射则用的是DNAT规则宿主机收到某个端口的数据再翻译成容器IP加容器端口。用iptables -t nat -L -n就能看到这些规则很多“为什么端口不通”的根源都能在这里看到端倪。重要提示如果宿主机同时占用172.17.0.0/16网段Docker会自动重新规划docker0的地址所以不要在任何脚本里硬编码172.17.0.1这个网关。2. bridge、host、none三大基础模式的取舍2.1 bridge模式默认方案能用但有瓶颈docker run不指定--network时容器会自动加入名为bridge的默认网络也就是docker0。对这种开箱即用的东西我的态度是能用但不适合作为长期主力。bridge模式最大的优点是简单-p 8080:80映射一下外部就能访问。但它的短板也很明显默认bridge不支持内置DNS服务发现容器之间不能直接用容器名互相访问除非用--link参数指定而--link这个老机制是静态的、只对单个容器生效在服务规模一大之后维护成本就会直线上升。另外所有默认bridge容器共享同一套iptables规则端口映射写在全局链上一旦有人误操作改动规则影响面是全局的。还有一个更易踩的坑默认bridge容器之间是互通的如果你只是想用Docker隔离几套环境都在默认bridge上跑那隔离基本上形同虚设。安全角度讲这种模式适合本地开发、临时测试不适合生产环境里互相依赖的多服务部署。2.2 自定义bridge网络我首选的日常配置自定义bridge和默认bridge最大的区别在于你是否自己定义了网络参数。生产中我几乎都会先创建一个独立的bridge网络再往里面放容器而不是裸奔在docker0上。创建时最常用的命令大概是这样的docker network create -d bridge \ --subnet 10.20.0.0/16 \ --ip-range 10.20.1.0/24 \ --gateway 10.20.0.1 \ app-net--subnet指定整个网络的地址范围--ip-range指定Docker自动分配容器IP时使用的范围--gateway是容器默认网关地址。我会专门把ip-range画出来是因为如果你的物理网络或者云VPC已经占用了某一段那就要避开。比如有人习惯从/16里分一小段给容器既方便路由又不会和宿主网络冲突。自定义bridge有默认bridge没有的三个核心优点一是同一网络内的容器可以直接用服务名互相解析这是自动的不需要额外配置二是网络之间天然隔离两个自定义bridge之间不互通比手工维护iptables省心得多三是容器可以动态加入和退出网络不用重建容器。一条命令解决问题docker network connect app-net my-container日常使用中我通常这样规划所有需要互相通信的容器放进同一个自定义bridge和外界需要交互的服务用-p映射纯内部组件不映射端口。这样既保证了通信又缩小了暴露面。2.3 host模式用隔离换性能先确认端口够用host模式直接用--network host启动容器。这时候容器不创建独立的网络命名空间直接使用宿主机网络栈容器监听的所有端口就是在宿主机端口上监听。IP地址也相当于宿主机地址容器里的服务外网直接就能访问。host模式的性能优势很直观没有NAT没有veth转发网络路径就是本机路径延迟和吞吐都很接近宿主机原生进程。所以我遇到对网络延迟敏感、需要低开销处理的批任务时会优先考虑host模式。缺点同样要讲清楚。第一端口是全局的两个都用host模式的容器如果同时监听80端口就冲突了第二个根本起不来。第二网络隔离约等于没有容器一旦被攻破等于直接在宿主机网络栈里操作。这种行为在安全要求高的环境里是要严格禁止的。还有一点在开发机上常坑人Docker Desktop for Windows和macOS里容器本质上是跑在一个Linux虚拟机中--network host指向的是那个虚拟机的网络栈而不是你开发机的网络栈。所以“host模式怎么和本机预期不一样”这类问题别急着怀疑Docker先搞清楚运行环境。2.4 none模式完全断网的“裸奔”场景--network none会创建一个只有回环接口的容器没有eth0没有外网访问。这个模式看起来没什么用其实在特定场景里非常好用。比如离线计算任务、敏感数据处理它们原本就不需要网络连接反正跑完把结果写到卷里就行那去掉网络就能减少很多攻击面和干扰。更高级的一种用法是容器先用none模式启动网络环境完全空白然后由你自己手工创建veth、网桥或者macvlan让容器网络完全受你控制。这种操作不常见但在排查特殊网络问题或定制网络方案时需要。普通开发者在正常业务里很少遇到理解它的存在和适用场景就够了。3. overlay网络跨主机通信的成熟方案3.1 VXLAN封装overlay的数据面原理单个主机上用bridge就够了一旦服务分布在多台宿主机上容器之间的通信就绕不开跨主机网络。Docker提供的成熟方案是overlay网络。overlay的核心思想是在物理网络之上再架一层虚拟二层网络。每个容器发出的以太网帧会被Docker封装在UDP包里通过物理网络传到目标宿主机再由目标宿主机拆包还原给目标容器。默认使用的封装协议是VXLAN外层UDP端口是4789。你可以在tcpdump里看到4789端口上有加密的VXLAN流量这往往意味着overlay网络在工作。为什么用VXLAN而不是直接改交换机配置VLAN因为VXLAN不需要底层网络结构配合只要机器间UDP能通就能把跨地域的宿主机拉到一个虚拟局域网里。好处是部署灵活坏处是每个数据包要多几十字节的封装头网络吞吐会略降延迟也会增加。这个代价对绝大多数业务是完全可以接受的。3.2 基于Swarm模式的overlay配置步骤标准做法是使用Swarm模式因为Swarm自带控制面能够把网络的配置和状态同步到所有节点省去了自己搭KV存储的麻烦。具体步骤不复杂首先初始化集群管理节点执行docker swarm init --advertise-addr--advertise-addr一定要指定正确如果机器有多个网卡不指定会导致节点之间找不到对方这是非常常见的坑。然后拿到worker节点的加入命令docker swarm join-token worker在worker节点执行返回的那个完整join命令完成节点加入。接下来创建overlay网络docker network create -d overlay --attachable demo-net--attachable这个参数非常重要它允许普通docker run方式启动的容器也能连接到overlay网络。如果不加默认只有Swarm服务docker service create创建的容器才能使用这个网络很多人在这一步踩坑。部署服务docker service create --name web --network demo-net --replicas 3 nginx跨主机容器之间就可以用服务名直接通信了。集群网络需要放行几个端口2377/tcp用于集群管理7946/tcp和7946/udp用于节点间通信4789/udp用于VXLAN数据面。防火墙和云安全组少了任何一个overlay都可能“半通不通”。3.3 不依赖Swarm的旧式方案与注意事项Docker 1.9时代还有一种老方案外置一个分布式键值存储组件把集群的key-value状态放在里面然后每个Docker daemon加上--cluster-store和--cluster-advertise参数再配合主机名解析来建立overlay网络。这个方案现在已经很少被新项目采用了。原因是Swarm或者Kubernetes这类编排器已经把控制面、服务发现、负载均衡全部集成好了你再自己维护一套KV存储用来做网络同步运维成本只会更高。我的建议是新项目直接用Swarm模式或者借助K8s的CNI老项目如果还在跑那套KV方案尽量找机会迁移别在新的技术栈里引入这种历史包袱。4. macvlan与ipvlan贴近物理网络的两种玩法4.1 macvlan模式给容器分配物理网段地址bridge和overlay都属于“容器私有IP NAT”的思路。而macvlan是另一种玩法它让容器的veth直接桥接到宿主机的物理网卡上。也就是说容器之间不再走docker0而是直接出现在物理网络中每个容器会拿到一个和宿主机同网段的IP并且有自己独立的MAC地址。创建macvlan网络的命令docker network create -d macvlan \ --subnet /24 \ --gateway \ --ip-range /27 \ -o parenteth0 mac-net-o parenteth0指定父接口是宿主机的eth0。容器启动时docker run --network mac-net --ip -d nginx这样外部网络可以直接访问这个容器IP不需要端口映射。适合的场景包括容器需要被现网网络设备直接纳管、依赖组播或某些二三层协议、容器安全策略需要按照物理IP做风控。前提是你有充足的IP资源宿主机网卡支持混杂模式而且网段规划经过允许。macvlan的缺点也很明显底层网络里会有大量新的MAC地址交换机MAC表压力增大运维同事可能找你麻烦IP资源不够时根本玩不转而且如果你所在的环境禁止非受管设备接入网络那就不要用。4.2 ipvlan模式共享MAC的取舍ipvlan是macvlan的兄弟区别在于所有容器共享父接口的MAC地址。这样就不会在交换机上产生大量MAC条目适合需要运行大量容器的场景。ipvlan有两种模式L2模式所有子接口共享父接口MAC行为上像二层交换机容器间在同一子网内可直接通信但广播和组播域是整个父接口。L3模式内核在父接口上做三层路由Docker自身就像一个路由器不同子网间的容器可以互相访问不依赖外侧交换机。创建命令类似docker network create -d ipvlan \ --subnet /24 \ --subnet /24 \ -o parenteth0 \ -o ipvlan_model3 ip-net注意ipvlan在云环境里的兼容性不如macvlan部分云厂商网络栈不允许这种操作。用之前先查一下当前环境是否支持别上线之后才反应过来。4.3 macvlan与宿主机互通的坑macvlan模式下容器默认不能和宿主机正常通信。原因是macvlan为了避免环路会拦截发往父接口MAC的帧导致宿主机上的服务访问不到同网段的容器IP。我第一次用的时候也被这个坑折腾了很久明明宿主机在两台容器都能ping通但宿主机自己就是ping不通容器。解决办法有三个方向。第一在宿主机上额外创建一个macvlan子接口配一个同一网段的IP用它来做宿主机与容器的通信。第二直接用ipvlan模式它没有这个问题。第三干脆不要依赖宿主机直接访问容器走服务发布入口。具体选哪种看你的实际场景。另外macvlan相当于把容器直接扔进了物理网络没有任何二层隔离如果这个网络本身是不可信的那就等于把服务裸奔在外部环境里安全上要慎重。5. 网络配置实操从命令到参数的完整记录5.1 常用network命令清单这里把日常最常用、最核心的docker network命令整理成一个速查表按使用频率排命令作用常见坑docker network ls查看所有网络无法知道哪些容器在用需要配合inspectdocker network inspect test-net查看网络详情含容器IP、子网、网关大网络时信息太长建议加--format按需查询docker network create -d bridge test-net创建网络不指定子网会从默认地址池分配docker network connect test-net container1动态把容器接入网络接入后需要重新检查容器网络配置docker network disconnect test-net container1动态断开连接断开后容器会立刻失去网络docker network rm test-net删除网络网络已绑定容器时删除会失败一条很实用的命令是格式化查看docker network inspect app-net --format {{range .Containers}}{{.Name}}: {{.IPv4Address}}{{end}}排障时用它一眼看全所有容器IP比在日志里翻半天省事。5.2 静态IP分配的正确姿势不少人在Docker里想给容器指定静态IP直接写--ip结果报错User specified IP address is supported only when connecting to networks with user configured subnets。原因就是默认bridge不支持静态IP。正确做法是先用自定义网络把子网规划好docker network create -d bridge --subnet 10.20.0.0/16 --ip-range 10.20.1.0/24 --gateway 10.20.0.1 static-net docker run -d --network static-net --ip 10.20.1.10 --name db-server mysql容器已经启动后也可以再补docker network connect --ip 10.20.1.11 static-net another-container静态IP的一个好处是方便配合外部防火墙做白名单另外一个好处是排障时IP可预测。但它也有麻烦一旦容器重建如果IP被别的容器占用起不来就是分分钟的事。所以我的建议是静态IP只给基础组件比如数据库、注册中心、网关普通业务服务让它自动分配就好。5.3 端口映射细节-p参数看起来简单但细节很影响排障。默认情况下写入的规则只覆盖TCP协议。如果服务用的是UDP比如DNS或日志采集器必须显式写docker run -p 8080:80/udp -p 8080:80 nginx此外-p 8080:80默认绑定在所有网卡上等于对公网开放。如果只想本机访问docker run -p 127.0.0.1:8080:80 nginx很多开发者在本地把服务绑定在回环地址上部署到服务器后外面访问不通第一反应是防火墙或者安全组问题实际上是自己当年的-p写法就没绑到外部网卡。用-P随机端口时Docker会把Dockerfile里EXPOSE的端口映射到32768以上的随机端口查看用docker port container-name。还要注意端口映射是通过iptables的DNAT实现的如果你在宿主机上自己清空了iptables规则或者用了某个脚本直接重启防火墙并重置链Docker映射的端口可能会全部失效。遇到“映射了但访问不到”不要只盯着容器看先确认iptables -t nat -L -n里还有没有对应规则。5.4 DNS与hostname配置容器里的DNS解析是网络配置里最容易被忽视的一环。默认情况下容器里的/etc/resolv.conf来自宿主机配置但有些情况下这个值不一定适合比如宿主机用了systemd-resolvedDNS指向的是一个本地缓存地址容器里无法正常访问。这时候可以在daemon.json里做全局配置{ dns: [, ] }然后重启Docker。也可以在运行容器时用--dns参数单独指定。同样--hostname可以修改容器内主机名--add-host可以往/etc/hosts里写入自定义记录。自定义bridge网络自带DNS解析能力同一网络内容器名可以直接被解析成对应IP。这个特性让服务之间通信不需要依赖固定IP所以我在微服务架构里基本上都是网络内部用服务名调用对外才用NAT或网关。MTU也是一个容易被忽略的点。某些云环境物理链路MTU是1400而Docker默认MTU是1500容器访问外网就会出现“部分网站能通、上传大包就卡死”的症状。改法是在daemon.json里加一行{ mtu: 1400 }6. 故障排查实录现象、根因与修复步骤6.1 排查工具集排查Docker网络光靠docker exec进容器里ping一下是远远不够的。容器里的基础镜像很多都不带ping、traceroute甚至没有ip命令。我建议先掌握宿主机层面的三件套第一nsenter。直接用nsenter进入容器的网络命名空间就能用宿主机的全套网络工具来排查容器内外的问题PID$(docker inspect -f {{.State.Pid}} container-name) nsenter -t $PID -n ip addr nsenter -t $PID -n route nsenter -t $PID -n ping这种方法不用在容器里安装任何额外软件是我最常用的方式。第二docker inspect。看网络名、IP、网关、DNS配置、端口映射这一步能确认70%以上的基础问题。第三宿主机抓包工具。tcpdump -i docker0抓容器通信数据tcpdump -i eth0 host 抓过度NAT之后的流量。定位到底是容器层面不通还是NAT之后不通这一步区分得越早排查越快。6.2 常见问题速查表我把实际中遇到过的高频问题整理成一张速查表可以直接对照现象常见根因排查方向容器A访问容器B的域名不通两个容器不在同一个网络或网络没有内置DNSdocker network inspect看各自所属网络放到同一自定义bridge端口映射后外部访问不了-p绑定在127.0.0.1、iptables规则被覆盖、云安全组未放行ss -ltnp看监听地址iptables -t nat -L -n查看DNAT规则容器访问外网时好时坏大包不通MTU不一致抓包看分片状态调整daemon.json mtu指定静态IP报错使用默认bridge网络创建自定义子网重新连接overlay网络跨主机不通防火墙未放行4789/7946或advertise-addr指定错误telnet测试端口检查udp连通性确认节点状态容器能ping通宿主机但宿主机ping不同容器macvlan模式的预期限制用ipvlan或增加宿主机macvlan子接口6.3 我踩过的一次整机断网案例说一个让我印象深刻的案例。某开发者在云服务器上装好Docker启动了一个容器并加了端口映射。一开始一切正常但某天服务器突然所有外部请求都超时重启Docker又恢复过一段时间又超时。第一次遇到这种问题我也以为是防火墙安全组的问题查了一圈没结果。后来仔细看iptables才发现容器里的服务在启动时会尝试修改容器内的iptables规则但由于容器默认没有NET_ADMIN权限它直接影响到的是宿主机不对其实真正原因是当时使用了host网络模式容器内的配置脚本直接改动到了宿主机的iptables规则把默认FORWARD策略设成了DROP容器和宿主机的数据通通被丢弃。这个案例给了我一条很重要的教训不管什么网络模式容器内执行的网络配置命令影响范围取决于它共享的是哪个命名空间。host模式下一句iptables命令效果等同于你在宿主机上执行。所以使用host模式时容器镜像是不可信的、镜像里包含能改网络栈的脚本那就要格外小心。知道自己用了什么模式才能知道出了问题该往哪个方向查这是最基础也最容易被忽略的经验。最后我自己用Docker网络的习惯是这样的单机场景一律自定义bridge把需要互通的容器收编到一个网络里跨主机场景优先overlay前提是确认好防火墙端口macvlan只在确实需要把容器IP直接暴露到物理网段时才使用。这样规划下来绝大多数容器通信问题都能在发生之前就避免掉。如果你也遇到莫名其妙的网络不通先别急着改代码把docker network inspect的结果贴出来看一眼往往答案就在那张表里。