在继续《docker》系列的时候我一直想单独把网络这一块拎出来写一写。不只是因为它在整个容器技术栈里的地位特殊更因为我见过太多人前期学docker时一路顺畅一到部署MySQL、Redis、GitLab这类需要对外提供服务、或者容器之间要互相通信的场景就开始卡壳。docker网络不通容器连不上宿主机这类问题几乎成了新手上路的头号拦路虎。这篇就结合我实际跑过的各种容器环境把docker网络这层窗户纸捅破从模型原理讲到具体踩坑再到最后的排查套路一条线走完。先说清楚这篇文章是给谁看的。如果你已经能docker run跑起来一个hello-world但对--network、bridge、host这些参数还是似懂非懂那就正合适。如果你正在部署微服务被跨容器调用、端口映射、IP固定这些问题折磨那也能从这里找到完整的思路。我不准备堆太多官方文档里能找到的定义而是会从为什么这么配遇到问题了怎么想的视角来聊很多内容是我自己反复试验后总结出来的跟纯翻译文档完全是两码事。1. Docker网络模型五个模式的核心理解1.1 桥接模式bridge是默认但未必最优Docker的默认网络是bridge这也是大多数人第一次接触容器网络时遇见的模式。在纯粹理解上你可以把bridge想象成一台物理交换机容器就像插在这台交换机上的主机每个容器有自己的虚拟网卡配上独立的IP地址并通过veth pair这种虚拟网线连接到一个叫docker0的网桥上。我在最初学的时候一直没明白为什么容器里的eth0跟宿主机上看到的veth开头接口是成对出现的其实它们就是一根虚拟网线的两头数据进来必须走完这一对才能从容器内到宿主机。实际使用中bridge网络解决的问题是隔离与互访。同一个bridge网络下的容器可以互相通过IP或容器名直接访问这一点对于本地开发、在单机上跑一组相关服务比如一个后端连一个数据库非常实用。但要注意默认的bridge网络有一个让人纠结的特性它不会自动启用基于容器名的DNS解析。也就是说你在默认bridge里通过容器名去ping另一个容器大概率会失败。我们后面会专门聊内置DNS这里先记住一个结论如果要用网络别名或容器名稳定互访优先新建一个自定义bridge网络而不是直接跑在docker0上我下面会说为什么。说到端口映射bridge模式也是最常用的方案。通过-p参数把宿主机端口和容器端口绑定这样外部访问宿主机IP加端口就能到达容器内部。有人问为什么容器内应用监听8080宿主机上还要映射一个8080因为容器有自己独立的网络命名空间端口在命名空间内部是隔离的不映射就等于与世隔绝。这里有个我踩过的坑-p参数绑定的IP如果是0.0.0.0默认意味着宿主机所有网卡IP上都会开放这个端口如果宿主机有公网IP这就在公网暴露了内网服务。我见过不止一个开发环境因此被扫描器盯上。记住能不绑0.0.0.0就用-p 127.0.0.1:3306:3306这样的形式只让本机访问安全上一个等级。1.2 主机模式host与无网络模式none的适用边界host模式是让我又爱又恨的一种模式。爱它是因为性能好、配置简单使用--network host启动容器容器不会拥有自己的网络命名空间而是直接使用宿主机的网络栈。此时容器内监听的端口就跟宿主机进程监听的端口完全等价不需要做任何端口映射直接访问宿主机IP加容器内端口即可。恨它是因为容器的隔离性在网络层面基本失效而且会出现端口冲突风险。我在什么时候用host呢一种情况是运行一些对网络性能极度敏感的应用比如需要高吞吐的缓存中间件用host能省掉NAT和端口映射用户态代理的开销。另一种情况是容器内需要接收到一些广播包或组播包比如某些服务自动发现协议在bridge模式下会被隔离掉host模式则能直接看到物理网络。但代价也很直观如果你在宿主机上已经跑了一个监听8080的服务再用host模式启动另一个监听8080的容器就会直接失败报端口占用。这对编排式部署不友好所以生产环境里我不会轻易用host除非有明确场景。none模式就单纯很多其实就是不配置任何网络。容器只有一个回环接口lo没有任何对外通道。这个模式看起来没用但在我做网络调试、抓包分析或者跑一些不需要网络的离线任务时反而能派上用场。加上--network none容器网络命名空间里干干净净排除掉网络因素方便我排查应用本身的问题。另外还有一个经典用法拿来创建一些初始化容器它只需要执行安装或初始化动作完成就退出不需要占用任何网络资源。1.3 overlay与macvlan跨主机与物理网络的新玩法如果你用Docker Swarm或者Kubernetes之类的东西做多主机容器编排那么overlay网络是标配。overlay网络能跨多台宿主机构建一个逻辑上的二层网络容器不管落在哪台机器上IP地址都能保持稳定且互相直达。它的底层是通过VXLAN隧道把数据包封装在宿主机物理网络里传输有点类似于异地组网软件穿越公网的感觉只不过隧道两端是Docker引擎自己管理的。我在Swarm模式里部署多服务时就为每个应用单独划一个overlay网络既实现了服务间隔离又能让不同主机上的容器通过服务名互相调用。需要注意的是overlay网络的性能会比bridge略低因为封装和解封装有额外开销如果对延迟极其敏感需要结合host模式和负载均衡方案一起考虑。macvlan模式则适合那些需要对容器分配物理局域网IP的场景。用macvlan可以让容器直接出现在宿主机所在的局域网中拥有一个独立的MAC和IP就像一台单独的物理设备。这个模式对有老旧依赖、习惯按IP直连的应用特别友好但配置时稍微粗心就会造成网络不通。我记得第一次配置macvlan时忘了宿主机也会占用一个局域网IP结果子网段选得不好宿主机和容器互相ping不通后来才发现父接口网络里需要专门给宿主机保留一个地址。macvlan还限制宿主网卡不能直接和容器通信这是它的一个硬伤需要额外用macvlan或vlan子接口解决。2. 网络配置的实操要点从创建到绑定2.1 docker network create 的常用参数与选择逻辑大多数情况下新建一个自定义网络比直接用默认网络更好用。原因很简单自定义网络自带DNS解析能力容器之间能直接通过名字互访而且可以指定IP段、设置网关整体的可控性和隔离性都比docker0好很多。我强烈的建议是在正式项目里一律用docker network create先建好网段再让容器跑进去而不是直接裸用默认bridge。语法上最常用的几个参数是docker network create --driver bridge \ --subnet172.28.0.0/16 \ --ip-range172.28.5.0/24 \ --gateway172.28.0.1 \ my-net这里说明一下每个参数的作用。--subnet用来定义网络CIDR段比如172.28.0.0/16这个网段会作为容器的地址池--ip-range则限定Docker自动分配IP时能在哪个小范围内挑选上面这个例子就是强迫自动分配的IP只在172.28.5.0/24内--gateway就是容器网络的网关一般取该子网的第一个可用地址。如果想在启动容器时用--ip指定固定IP这个固定IP必须落在subnet里但可以不在ip-range范围内因为ip-range只约束自动分配逻辑。选择网段时有个经验一定要避开宿主机的物理局域网段和已有的其他Docker网段否则会出现路由冲突。我习惯用172.16-30开头的网段把不同项目拆成不同子网这样通过IP就能大致判断是哪一个环境的容器。如果是纯跑在云主机上还要留意云厂商的内网IP段通常也在172开头最容易撞车先去ip addr看一眼再定网段。2.2 容器连接网络--network 与 --ip 的坑创建好网络后启动容器时用--network连接就行。但这里有一个多个网络的坑Docker允许一个容器同时接入多个网络容器可以当作路由器使用在不同网络之间转发数据。我第一次看到这个特性时觉得非常强大但实际用起来发现一个小问题如果容器接入多个网络默认路由会变得复杂此时访问外部网络时走哪条路由往往不容易判断容易造成能ping通内网却上不了外网的怪问题。所以按我的习惯一个容器尽量只连接一个网络除非真的需要承担多网络网关的角色。--ip参数可以配合指定固定IP但有个前提条件只能给指定了自定义网络的容器动态设置。你在默认bridge上用--ip会报错因为默认bridge不支持静态IP配置。另外固定IP要写到启动参数里每次都能生效但Docker不会阻止其他自动分配的容器占用这个IP我之前就遇到过两个容器的IP冲突导致间歇性访问超时。解决方式是配合--ip-range把自动分配的IP范围与静态IP区段分开比如静态IP用172.28.0.10-50自动分配用172.28.5.0/24互不打扰。还有一个容易被忽视的细节容器启动时如果网络还没准备好会报network not found或invalid network reference。这不是玄学往往是因为网络名写错了或者是正在重启阶段。遇到这种报错先docker network ls看一眼确认名字完整带不带数字或符号。因为网络名是允许带下划线和点的但如果你用的是短横线有些老旧版本的docker可能会有解析问题现在新版倒是没这个毛病了。2.3 端口映射的底层原理与常见误区端口映射看似简单但理解它的底层实现能帮你少踩很多坑。Docker做端口映射依赖两条链一个是NAT规则一个是docker-proxy进程。默认情况下Docker会同时使用iptables做DNAT规则和启动一个docker-proxy用户态代理进程。当外部数据到宿主机端口docker-proxy先把流量接住再转发给容器的IP和端口而iptables的DNAT逻辑则把目标地址改写成容器的IP直接在内核层面完成转发。这带来了一个很常见的误区很多人以为只要-p映射了端口从宿主机访问localhost端口就能到容器其实在部分环境下docker-proxy会帮你实现但在某些修改了iptables策略或关闭了用户态代理的环境里就会不通。我在一个经过安全加固的服务器上遇到过这种问题-p参数显示映射成功但是从宿主机curl localhost:8080始终连接被拒容器内部却一切正常。后来排查发现是宿主机的iptables有一条FORWARD默认DROP策略把docker-proxy转发容器的数据包拦住了。解决方法是把docker-proxy重新启用或者在iptables里给Docker相关的链加放行。这个案例说明了一个道理端口映射不是你加了-p就完事防火墙、内核参数ip_forward、SELinux这些都会影响最终的通断。还有一个常见的误区是误以为-p可以修改容器内监听地址。容器内应用监听的一般是0.0.0.0或127.0.0.1如果你在容器内只监听127.0.0.1那么外部即使做了-p映射也无法访问到这个容器。这种情况在调试MySQL和Redis时特别多见我在后面章节会专门讲具体例子。3. 容器间通信的三种方式同网段、跨网段与别名3.1 基于名字的解析内置DNS与link的替代方案解决容器间互访最优雅的方式是基于名字的解析。Docker从1.10版本开始内置了DNS服务容器在自定义网络里启动后会把该网络内的其他容器名和别名自动注册进去所以只要在同一个自定义网络上直接用容器名就能ping通。可能有些人还在用老掉牙的--link参数。--link确实能实现名字解析但它只能在创建时指定而且只是往/etc/hosts里写一行映射容器重启后这个映射可能会失效跨主机更是完全没用。相比之下自定义网络的内置DNS完全动态容器启停都会实时更新这就把老方案彻底淘汰掉了。我在项目里全面转向自定义网络后再也没因为容器重启导致hosts过期而头疼过。在这个基础上还有一个alias参数值得深入了解。通过--network-alias容器别名可以让一个容器拥有多个名字。这个功能在服务升级或A/B测试时特别方便比如你有一个旧容器myapp-v1新容器myapp-v2都可以设置网络别名myapp那么其他服务只需要始终访问myapp流量实际会转发到其中某一个容器上。配合负载均衡组件可以做出滚动升级的效果而不用修改调用方的配置。3.2 多容器应用Compose网络自动管理的细节说到容器间的网络管理我不得不提Docker Compose。很多新手用Compose时根本不会手动创建网络但Compose在后台已经默默帮你做了这件事。每个Compose项目默认会创建一个以项目名命名的网络项目内所有服务默认都挂载到这个网络上所以服务之间直接通过服务名互相访问。我第一次用Compose编排一个前后端加数据库的应用时就吃了一次暗亏。我在后端服务配置里用localhost去连数据库一直连不上后来才反应过来在Compose项目网络里后端容器访问数据库必须用数据库的服务名比如db而不是localhost。localhost在容器里指的是容器自身完全隔离在它自己的网络栈中不会指向其他容器。这几乎是所有人刚上手时都会犯的错你把localhost当成宿主机了吧其实在容器里它就不是。Compose网络还有一个好处它会自动更新DNS。但有个例外情况如果服务设置了container_name去指定一个固定的容器名那么其他服务仍然可以通过这个名字访问但不建议这么干。因为container_name会限制服务的自定义扩容如果有多个实例容器名就会冲突反而是使用服务名更通用。Compose文件里的networks字段可以指定自定义网络如果想把多个项目拉通就需要显式把外部网络声明成external或者预建好的网络否则项目之间互不可见。3.3 网段规划与IP固定让服务地址稳定在测试环境里容器IP漂不漂都无所谓反正可以走服务名。但有些老旧应用或者对配置要求很死的中间件还是希望IP永久不变。这种情况下我强烈建议在一开始就做网段规划避免后续改配置导致连锁反应。我的习惯是每个业务域划分一个独立的/24子网子网里单独分一个静态IP区域和一个DHCP风格分配区域。比如数据库这类核心组件用固定的--ip 172.28.0.10应用服务跑在自动分配段172.28.5.0/24内反正应用可以通过数据库固定IP访问不需要稳定自身的IP。这样既保证了数据库地址稳定又让普通服务可以自由伸缩。如果你反过来给所有容器都配固定IP那么扩容一个新实例时就要小心翼翼地找空闲IP极易出错。还有一点需要注意如果你修改了宿主机防火墙规则或者把Docker网桥的MTU调低所有已有容器的网络不会立刻感知。按我的经验调整MTU之后最好重建容器否则容器内会一直出现诡异的大包不通问题。网段规划的时候也要把MTU因素考虑进去我后面测速章节会专门讲MTU的影响。4. 网络不通排查实战与经验清单4.1 从docker network inspect到nsenter的排查链路遇到网络不通别慌我有一套固定的排查路径。第一步永远是docker network inspect这是查看网络信息的起点。docker network inspect my-net这个命令会输出该网络的名称、驱动、IPAM配置、子网、网关以及所有挂到此网络的容器及其IP地址。从这里你能立刻确认容器是不是真的在这个网络里IP有没有撞车。我看这个输出的习惯是重点关注Containers字段如果里面没有你想要的那个容器不用往下排查了肯定是容器没连接上来去检查启动参数。接下来确认容器内部的网络状态docker exec container ip addr docker exec container ip route有时容器里没有ip命令可以用cat /proc/net/dev查看接口统计数据用cat /proc/net/route查看路由表。我遇到过很多次容器内没装网络工具的情况这时候直接用nsenter进入宿主机网络命名空间来看得更清楚。命令大概长这样nsenter -t 容器进程PID -n ip addrPID从docker inspect里拿或者直接docker inspect --format {{.State.Pid}} container。这种方式相当于直接跳到容器的网络命名空间里执行命令不受容器内缺失工具的限制。我第一次用这个命令时就像拥有了一把万能钥匙很多隐藏的网络问题瞬间清楚了。接下来的关键判断点是联通性测试docker exec container ping -c 3 网关IP docker exec container ping -c 3 目标容器IP docker exec container ping -c 3 8.8.8.8注意IP和域名要分开测。如果网关通但容器之间不通问题往往出在隔离、防火墙规则或IP冲突上如果网关都不通多半是网络创建或桥接本身就有问题此时回到底层检查一下docker0的状态必要时直接重建这个网络。一个我重复很多遍的经验是不要用容器内默认的DNS去直接ping域名DNS解析失败会和其他网络问题混在一起手动dig一下把解析和连通分开排查。4.2 典型案例MySQL、Redis、GitLab部署中的网络坑网上经常看到有人问docker安装MySQL8.0并使用的问题我也踩过。最经典的坑是容器启动成功但外部工具连接不上。排查时发现多半是因为MySQL容器只监听在127.0.0.1上。原因是在容器内指定了bind-address127.0.0.1或者是我启动容器时忘了做端口映射也有一种情况是容器内有自定义配置覆盖了默认监听地址。解决办法是在运行命令时通过环境变量和配置文件明确绑到0.0.0.0并对-p参数指定的宿主机端口做合理选择。另外一个关于MySQL的坑是网络通了但字符集不对或者加密插件不支持。这些虽然不直接是网络问题但在排查连接错误时会被误认为是网络不通。我遇到过一个错误是Host x.x.x.x is not allowed to connect to this MySQL server这其实是MySQL自身的访问控制跟docker网络无关但如果你对docker网络不熟悉很容易在错误的方向找原因。首先排除容器网络通不通然后进容器看看MySQL的user表的host字段这是经验。Redis的部署相对轻量但我也遇到过在macvlan网络下数据能通ping不通的情况。后来定位是macvlan父接口的物理交换机禁用了arp广播导致容器无法正确完成ARP解析。另外还有一次是在bridge模式下启动了一个Redis主从主从容器都能各自独立启动但replica访问不到主节点。原因是我把主节点固定了IP但只用了旧IP容器重建后IP变了。后来我把主从都改成别名访问主节点地址用网络别名从节点配置里引用别名问题就解决了。Redis主从模式下的网络配置更推荐在同一网络下互用服务名省去IP漂移的烦恼。GitLab社区版的docker部署网络坑主要体现在端口占用和路径上。GitLab默认会监听80和443端口如果你宿主机上已经跑了nginx一启动就会报端口占用。此时我一般把宿主机映射成自己的高位端口比如8443:443和8888:80然后用nginx做一层反代把外部域名转发到这些端口。还有一点是GitLab初始化配置里的external_url这个地址设置不正确会导致页面跳转和SSH连接都走错端口。这个external_url本质上就是网络访问路径设置它的时候要结合你最终对外暴露的域名和端口而不是docker内部映射的裸端口。4.3 网络性能与测速不要只看理论带宽docker网络性能这个话题不算特别大但值得专门一说。很多人测速时只看吞吐带宽而不了解背后的网络路径对延迟的影响。我们常用iperf3在容器间测速在bridge模式下流量要经过宿主机内核的NAT和路由转发性能理论上低于host模式。实测下来bridge模式如果默认网桥的收队列处理不过来小包延迟会明显增加。有一个最容易被忽略的因素是MTU最大传输单元。Docker默认bridge网络的MTU是1500这在大多数环境下没事。但如果你宿主机上的物理网络MTU被设置成了1450比如在某些云平台上可以通过DHCP获取到更小的MTU那么Docker网桥默认的1500会产生分片导致大量数据包需要重组吞吐量骤降甚至出现连接超时。我在一个云服务器上部署应用时发现容器内的下载速度只有宿主机的一半排查了一圈最后把Docker的MTU调成和物理网卡一致速度立刻就正常了。调整MTU的方法是在创建网络时指定或者在Docker守护进程配置里设置全局的mtu参数。创建网络的命令是docker network create --driver bridge --subnet172.28.0.0/16 -o com.docker.network.driver.mtu1450 mynet注意用-o参数给驱动传com.docker.network.driver.mtu。如果是Compose里也可以在networks下面用driver_opts配置。这种细节官方文档其实写得很隐晦我是在看了源码和社区方案之后才搞明白的所以在这里特意标出来。还有延迟问题。如果你做的是低延迟交易系统级别的服务建议不要用默认bridge直接把容器放到host网络或采用物理机进程部署。而那些对网络调度和隔离要求高的微服务反而更依赖overlay网络因为它的控制面是分布式管理的比单纯bridge在跨节点场景下更容易保证路由一致性。5. 额外补充宿主机防火墙与Docker网络的相互作用5.1 iptables与docker-proxy的安全细节Docker在启动时会主动修改宿主机的iptables规则这是它管理端口映射和网络隔离的底层机制。很多人一配置iptables就崩溃因为docker会动态插入很多规则。有时候我们为了加固服务器会把iptables的FORWARD链默认策略改成DROP结果发现所有已发布端口的容器外网访问全部失效就是这个原因。我的实践经验是不要直接在宿主机iptables的FORWARD链上粗暴drop而应该对Docker生成的链元素做精细管理。Docker会使用DOCKER链和DOCKER-USER链如果你要自定义策略优先操作DOCKER-USER链因为这个链里的规则不会被docker内部管理逻辑覆盖。比如你想拒绝某个IP访问宿主机上的容器服务只需要iptables -I DOCKER-USER -s 1.2.3.4 -j DROP这样就能安全地作用于所有通过Docker发布的服务同时不影响容器间通信。我用了这个技巧后再也不用担心重启docker导致自定义规则被冲掉了。另一个安全细节是docker-proxy进程。这个进程本身是个用户态进程会监听你通过-p映射的端口。如果你不需要它可以在docker daemon.json里把userland-proxy设置为false这样Docker就只依赖iptables的DNAT规则做转发少一层代理性能和安全性都有提升。但要注意这要求宿主机的iptables有正确的DNAT配置如果你在诡异环境下运行关闭userland-proxy可能导致localhost访问映射端口失效所以我一般建议先测试安全场景下再关。5.2 在部分环境中NAT导致的问题与回避NAT网络地址转换是Docker网络里绕不开的话题。默认bridge模式下容器访问外部网络是通过docker0网关做源地址NAT这样外部看到的源IP是宿主机IP而不是容器IP。这个特性大多数时候是好事但会让你在做基于IP的限制时犯迷糊比如服务端只允许某个数据库IP访问你却拿不到真实的容器IP。更麻烦的是当你在NAT后面再套一层网络比如容器内的应用去访问一个在内网里只有宿主机能通的目标IP容器默认是走不进那个目标IP的因为容器IP段和那个内网段之间没有路由关系。解决思路有三类第一把目标网络配置在宿主机上并docker容器的路由指向宿主机网段第二用host模式直接拿宿主机网络栈访问第三通过docker network connect把容器同时连到接入该网段的宿主网络设备上。实际操作用第一类比较多但要注意静态路由的持久化问题容器重启后路由可能被重刷最好用系统级工具做好管理。我在远程办公室开发时容器要访问公司内网的某个服务宿主机有公司内网IP但容器只能访问外部网络。后来我直接把容器的默认路由改成宿主机网卡并手动添加了内网网段的路由才让容器有了内网访问能力。这个方案比较hack但胜在简单只适合小范围自用生产环境还是应该走overlay或制定严格的网络路由策略。在云环境里还有一类NAT问题通过-p映射端口容器返回数据包做反向NAT时如果涉及UDP协议可能因为连接跟踪表超时导致丢包。比如运行一些自定义UDP服务如果客户端长时间不发包服务端要回数据时conntrack表里的旧状态已经过期数据包会被当成无效包丢弃。这种问题的排查思路是nstat看conntrack相关的计数器适当调整net.netfilter.nf_conntrack_udp_timeout。这是一个比较二次元的坑但遇到过一次就会牢记在心。最后分享一个我个人的排查心得docker网络问题最怕的是堆着问题不看日志。无论遇到什么奇怪的错误先从docker network inspect看两个对象的网络状态再docker logs看容器是否正常启动最后才是抓包。抓包工具可以用tcpdump在宿主机上抓docker0接口的包或者抓容器内veth接口的包定位到数据包到底在哪一跳丢的。抓好包之后90%的问题都会慢慢浮出水面剩下的就是耐心和细心了。这套流程我反复用了几年从本地开发到生产环境始终有效。