☰
Docker网络模式详解与容器互通实践
2026/10/22 3:25:14 网站建设 项目流程

1. Docker网络是怎么一回事:先搞懂容器看到的“世界”

很多刚接触容器的朋友,明明镜像也拉了、容器也跑起来了,但一到“网络”这一步就卡壳。容器之间怎么互相访问?容器能不能被宿主机之外的其他机器访问?为什么我起的两个容器用IP地址ping不通?这些问题的根源,都在于没有理清Docker网络的底层逻辑。

先把话说直一点:Docker默认创建的每一个容器,都像一个只有一张网卡的迷你主机。这张网卡是虚拟的,接在一个叫docker0的虚拟网桥上。只要容器都挂在这同一个网桥上,它们彼此之间就能用IP直接通信;一旦脱离了同一个网桥,或者跨了宿主机,通信方式立刻变复杂。

我这里讲的都是实实在在的默认行为。Docker安装好之后,宿主机上会多出一个名为docker0的网络接口,IP段通常是172.17.0.1/16。你在宿主机上执行ip addr show docker0就能看到它。这个网桥就是所有默认容器网络的“总闸”。

容器启动时,Docker会从docker0的地址池里分一个IP给容器,比如172.17.0.2。容器内部的eth0网卡、宿主机上的veth虚拟网卡、docker0网桥,这三者构成了一条数据通路。数据包的走向大概是:容器eth0 → veth虚拟网卡 → docker0网桥 → 宿主机的网络栈 → 最终到达外部网络。

这里面有一个天然的设计:容器和宿主机共用一个内核网络栈,所以容器访问宿主机、宿主机访问容器,都不需要额外配置。但容器之间默认只保证“同网桥互通”,跨网桥就要看你后面怎么规划了。

我建议初学者先做一个最简单的动作:启动两个nginx容器,然后用docker inspect查一下它们的IP,在其中一个容器里去ping另一个的IP。这个动作能帮你直观理解“同网桥互通”的含义。如果这一步通了,后面学自定义网络会顺利得多。

2. 一张图理清五种网络模式,别再背概念

先把最常用的五种网络模式拉出来对比一下:bridge、host、none、container、overlay。我做了个表,方便对照关键差异。

网络模式本质容器是否拥有独立IP能否跨宿主机适用场景
bridge通过docker0或自定义网桥是否单机多个容器互通
host直接使用宿主机网络栈否否对网络性能要求极高
none完全没有网络接口否否纯离线计算、安全隔离
container与指定容器共享网络栈否否依赖代理、调试抓包
overlay跨主机虚拟网络是是Swarm、K8s集群容器通信

2.1 bridge模式:默认,但默认不等于最优

bridge模式是Docker默认的网络模式。刚才说过,默认创建的网络就是docker0这个网桥。它的核心机制是NAT(网络地址转换),我现在把这个说得更实用一些。

当容器要访问外网(比如apt-get update、拉取外部API)时,数据包源地址是容器IP(比如172.17.0.2),这个地址在宿主机之外是不可路由的私有地址。Docker在宿主机内核里做了iptables规则,把源地址改写为宿主机IP,同时记录连接状态。外网返回数据后,再根据记录映射回容器的IP和端口。这就是容器能上网的最核心原因。

但从外部主动访问容器呢?单靠NAT做不到,因为你没有“入口”。这时候就得用端口映射,即-p参数。-p 8080:80本质上是往iptables里插入一条DNAT规则:宿主机8080端口收到的流量,目标地址改成容器IP的80端口。所以外部主机访问宿主机IP:8080,才能真正到达容器。

我在实际项目中见过不少问题出在端口映射上。比如映射的宿主端口被占用,或者防火墙没放行。另外要留意,-p只写了宿主机端口时,会随机映射到一个高位端口,不便于对外提供服务。所以生产环境里我习惯显式指定端口,比如-p 80:80,同时用-p 127.0.0.1:8080:80的形式只对回环地址暴露,增加一层安全约束。

2.2 host模式:没有独立IP,千万别误用

host模式就是容器直接使用宿主机的网络命名空间,容器没有自己的IP,直接共享宿主机的IP和端口。它的特点是性能好,因为数据包不用过NAT和网桥,延迟低、吞吐高。

但共享意味着端口冲突风险。你的容器里如果监听80端口,宿主机上就不能再监听80端口;同一个宿主机上,两个host模式的容器不能同时监听相同端口。我曾在压测场景里选用host模式跑高性能服务,确实比bridge模式快那么一点,但也就剩这一个优势了。这种模式适合对网络性能极其敏感的中间件(比如某些缓存服务),或者不需要任何端口隔离的调试场景。

2.3 none模式与container模式:两个“特殊用途”选手

none模式很好理解:不给容器配任何网络接口,只有回环接口lo。这种模式下的容器既不能访问外网,也不能被外部访问。典型的用法是跑一次性批量计算任务,或者做高安全性的隔离沙箱。因为没网,连系统包都装不了,必须提前准备好镜像。

container模式则跟另一个容器共享网络栈。比如我起了一个抓包调试容器,它监听在某个网卡上,另一个业务容器用--network container:业务容器名加入它的网络栈,这样调试容器就能看到业务容器的进出流量。还有一种常见用法是让某个容器作为其他容器的“代理网关”,业务容器全部绑定到它上,统一出口IP。

我提醒一下:container模式共享的不只是IP,还有端口和网络栈。也就是说,两个容器不能监听相同的端口,否则会冲突。设计服务时要把端口规划进架构里,临时替换容器时也要小心。

2.4 overlay模式:跨主机网络的唯一正解

overlay网络是Swarm服务和K8s里的核心网络方案。它通过VXLAN等技术,在两台物理机之间建立一个逻辑上的二层网络。每个加入同一个overlay网络的容器,都拥有同一个网段内的IP,不管这些容器分布在哪台宿主机上,彼此都能直接通信,就像是连在同一个交换机上。

对于只想管理几个容器的个人项目,overlay模式有点重,通常需要先初始化Swarm集群才有意义。但对于微服务部署在多台机器上的场景,overlay是首选。我后面讲容器互通时会提到单机的方案,如果你确实到了多机部署阶段,记得回头把overlay网络单独研究一下,核心配置项是加密、子网和控制面端口。

3. 容器间网络互通,从这里动手实操

学完模式概念,现在上手解决实际需求。最常见的诉求就是:我起了两个容器(比如一个后端接口、一个数据库),它们之间怎么互通?我把单机与跨主机的场景都演示一遍。

先说我遇到过的一个典型报错:两个容器都使用了默认的bridge网络,我用docker run -d --name app-a nginx和docker run -d --name app-b nginx启动,然后在app-a里ping app-b,结果报错ping: app-b: Name or service not known。

这个报错本质上不是网络不通,而是DNS解析不到名字。默认bridge网络的DNS解析规则不够灵活,它只能按容器IP来解析,不支持通过容器名去找。这也是Docker官方文档里明确提示过的限制。所以我的建议是:别在默认bridge网络上折腾名字解析,直接用IP,或者尽早换成自定义网络。

3.1 直接使用IP访问容器

最简单粗暴的方案是直接使用容器IP访问。启动容器后,执行docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' app-a,拿到app-a的IP(比如172.17.0.2),然后在app-b里执行curl http://172.17.0.2,业务能通。

这个方案的问题非常实际:容器重建后IP会变。我每次重启服务都要去查一遍IP,手工维护成本很高。如果你只是临时调试,可以这么干;要是写成脚本或配置里写死IP,三天后服务起不来,别怪我没提前说。

3.2 创建自定义bridge网络:容器名即可互通

自定义bridge网络是我个人最推荐的做法,也是解决容器间通信的“标准答案”。

先创建一个自定义网络:

docker network create --driver bridge --subnet 172.28.0.0/16 --gateway 172.28.0.1 my-net

然后启动两个容器,都接入该网络:

docker run -d --name app-a --network my-net nginx docker run -d --name app-b --network my-net nginx

在app-a里直接访问app-b,命令如下:

docker exec app-a ping app-b

这时你会发现能ping通了,而且不需要用IP。原因是自定义bridge网络自带内置DNS,Docker会维护容器名和IP的映射关系,所以容器名可以被解析。

如果你不想创建时手动指定子网,直接执行docker network create my-net也行,Docker会自己分配一段网段,但那样IP地址不直观。生产上我倾向于显式指定子网和网关,方便后续写防火墙规则和监控。

3.3 自定义bridge网络到底比默认好在哪

自定义bridge在功能上比默认docker0多了一块:内置DNS解析、更灵活的隔离范围、支持动态摘除容器不中断其他连接。

跟默认bridge比,自定义网络还有一个不小的优势:可以通过--link去连接那些不在同一网络的容器,但这个选项已不建议使用,属于历史遗留。更现代的做法是让容器加入同一个自定义网络,然后依赖DNS名字互相访问。

我把这个方案再讲深一点。一个容器可以同时加入多个网络,也就是说,它可以既在默认bridge里,也在自定义网络里。这样设计能实现“部分互通、部分隔离”的效果。比如数据库容器只接入一个内部网络,不直接暴露端口;接口容器同时接入内部网络和外部网络,通过端口映射对外提供服务。这种拓扑比乱开端口安全得多。

3.4 跨宿主机容器互通:overlay网络的初体验

如果你有两台服务器,且都加入了同一个Swarm集群,就可以创建一个overlay网络来连通跨主机的容器。

docker network create --driver overlay --attachable my-overlay

创建成功后,把服务部署到不同节点,只要都连入my-overlay网络,它们就能通过服务名互相访问。我在实际项目里用docker service create --network my-overlay --replicas 2 --name api-service 某个镜像来创建两个副本的服务,两个副本跑在不同的宿主机上,它们之间通过overlay网络直接通信,无需干涉宿主机的iptables规则。

如果是K8s环境,Docker的overlay模式通常作为底层容器网络的实现细节,上层一般用CNI插件来管理。对于纯Docker环境,overlay需要Swarm模式比较顺手。所以如果你只是单机两台机器想简单互通,还有一个比较省事的做法:直接在一台宿主机上用--network host启动服务,再从另一台机器访问宿主IP加端口。但这样不够优雅,也失去了网络隔离的意义。我的个人判断是:跨主机场景只要规模上了三台以上,就直接上Swarm吧。

4. 自定义网络与网络打通实操

自定义网络不只是“起个名字”这么简单,它的价值在于规划网络拓扑、实现隔离和打通异构网络。

4.1 如何规划自定义网络的网段与网关

创建自定义网络时,选网段要避开可能冲突的局域网段。比如你所在办公环境是192.168.1.0/24,Docker默认的172.17.0.0/16跟你办公网不冲突,但如果你刚好有一台路由器用了172.17.0.1,那就要小心了。我习惯把自定义网络规划在172.28.0.0/16或10.10.0.0/16这种相对冷门的段,给未来扩展留空间。

创建命令加上更细的参数:

docker network create \ --driver bridge \ --subnet 172.28.0.0/16 \ --ip-range 172.28.5.0/24 \ --gateway 172.28.0.1 \ my-net

这里的--ip-range用于限定Docker自动分配IP的范围,防止把整个172.28.0.0/16都分配出去。如果你的自定义网络里会跑很多容器,可以按业务模块再拆几个子网,比如给数据库一个段、给应用一个段,这样流量控制和安全策略都好做。

4.2 容器如何加入已有自定义网络,以及如何退出

把容器接入自定义网络,用docker network connect命令;断开网络用docker network disconnect。

docker network connect my-net app-a docker network disconnect my-net app-a

这两个命令不会重启容器,对正在运行的业务影响很小,生效是及时的。这在实际运维中很有用。比如某个容器本来只在默认bridge里,后期需要跟另一个自定义网络里的服务互通,不需要重建容器,直接docker network connect即可。我多次在服务不中断的情况下打通了不同容器网络。

4.3 网络打通:跨不同自定义网络的容器和主机互通方案

有时候会遇到两个容器分别处于不同自定义网络的情况,比如A容器在net-a,B容器在net-b,它们默认老死不相往来。解决办法有两条路。

第一条路是让A容器同时加入net-b网络。注意,docker network connect net-b A执行后,A会获得一个属于net-b的IP,于是A可以直接访问B,B也可以访问A。

docker network connect net-b A docker exec A ping B

第二条路是设置系统路由或者用iptables转发,但这种方式突出一个“能通但不好管”,我不建议在生产环境用,还是加一条docker network connect来得清爽。

如果你非要通过宿主机端口转发的方式访问另一个网络的容器,那也行。比如让宿主机把18080端口映射到net-b里某容器的80端口,但这样访问就要经过宿主机这一层,效率略低,Ping不通也是正常,因为两边的二层域不同。

4.4 容器访问宿主机的服务,这条路径最容易踩坑

容器访问宿主机上跑的服务(比如宿主机里有一个Redis监听在6379端口),在bridge网络下,可以直接访问172.17.0.1或宿主机局域网IP。为什么?因为docker0网桥的网关就是172.17.0.1,宿主机在这个网段上有IP,容器访问这个IP自然能到达宿主机的网络栈。

但如果宿主机上监听的是127.0.0.1而不是0.0.0.0,容器就无法访问,因为回环地址只在宿主机本机内有效。解决方法是让服务监听在0.0.0.0或内网IP上,并做好访问控制。另一个常见的坑是防火墙或安全组拦截了来自docker网桥的流量,导致容器内能ping通网关,但访问不到宿主端口。遇到这种情况可以先查看系统防火墙规则,确认docker0是否被放行。

5. 实战:搭建一个“前端+后端+数据库”的三容器网络

动手做一个完整小项目,把上面的知识串起来。场景:一个Web应用,分为前端容器、后端接口容器、数据库容器。目标:后端能访问数据库,前端能访问后端,但数据库不对公网暴露。

5.1 创建网络与服务容器

先创建两个自定义网络:front-net和back-net。

docker network create front-net docker network create back-net

数据库容器加入back-net:

docker run -d --name db --network back-net -e MYSQL_ROOT_PASSWORD=yourpass 某个mysql镜像

后端容器同时加入两个网络:

docker run -d --name backend --network back-net --network-alias backend-api 某个后端镜像 docker network connect front-net backend

前端容器只加入front-net:

docker run -d --name frontend --network front-net -p 8080:80 某个前端镜像

这里的关键点是后端容器在退出back-net之前先连入front-net,因为容器运行时可以随时连接新网络,不会影响原有连接。--network-alias给后端容器在back-net里加了个别名,便于数据库容器通过backend-api访问它。

5.2 验证网络隔离与互通结果

验证数据库是否只允许back-net里的容器访问,要求frontend容器无法直接连接数据库的3306端口。实际上因为frontend和db不在同一网络,前者用db这个域名解析都解析不到。这就达到了隔离效果。

验证后端能否连接数据库,命令如下:

docker exec backend ping db

能ping通,因为两者都在back-net里。再验证前端能否访问后端:

docker exec frontend curl http://backend:8080/health

能通,因为backend已经在front-net里。这就实现了一个相对合理的三层网络结构。

5.3 这套架构的数据流向解析

我以一次用户请求为例说明数据怎么走:

  1. 浏览器访问宿主机IP:8080,nginx前端容器接收请求。
  2. 前端容器通过backend-api这个名字访问后端容器,数据包从frontend的eth0发出,经过front-net网桥,转发到backend容器在front-net里的虚拟网卡。
  3. 后端容器接收到请求后,又通过db这个名字访问数据库容器,走的是back-net网桥。
  4. 数据库完成读写后按原路返回响应。

整个链路里,db容器没有映射宿主端口,所以外部无法直接接触数据库,减少了暴露面。这套拓扑在生产里很常见:存储层躲在内网,业务层半开放,入口层对外。

6. 常见问题排查与避坑实录

6.1 为什么容器之间ping不通?优先级从高到低排查

按下面这个顺序去查问题,基本能定位绝大多数网络故障:

排查步骤操作命令可能原因
1. 查看容器IPdocker inspect -f '{{.NetworkSettings.IPAddress}}' 容器名容器没IP说明网络没接好
2. 确认同网络docker inspect 容器名查看Networks两个容器不在同一网络
3. 测试网关连通docker exec 容器名 ping 网关IP网桥或iptables异常
4. 测试DNS解析docker exec 容器名 getent hosts 对方容器名自定义DNS没生效
5. 确认防火墙系统防火墙状态、安全组规则宿主机防火墙拦截

在自定义网络里还能ping不通,大概率是容器不在同一个自定义网络里,或者防火墙拦截了docker0的流量。

6.2 端口映射后外部仍无法访问,可能是什么原因

我见过最多的情况是:docker ps里明明显示了0.0.0.0:8080->80/tcp,但外部浏览器就是打不开。原因排查如下:

  • 宿主机防火墙没有放行8080端口。这里注意:ufw或firewalld默认策略可能拦住Docker的端口映射。
  • 容器内应用没监听在80端口,或者监听在127.0.0.1:80。Docker的端口映射只改目的地IP,改不了容器内应用绑定的地址。
  • 云平台的安全组没放行对应端口。改完Docker配置还要看安全组规则。
  • 映射到宿主端口被占用。执行ss -lntup | grep 8080确认。

6.3 容器重启后IP变化导致的配置问题怎么根治

如果你在配置里写死了容器IP,或者数据库连接串里写了IP,容器一重建就全断。治本方案是别依赖IP,改用自定义网络+容器名。比如连接MySQL的连接串写成host=db,而不是host=172.28.0.3。

如果历史系统一定要固定IP,那可以在创建容器时手动指定IP:

docker run -d --network my-net --ip 172.28.0.10 某个镜像

但要注意,这个IP必须在--ip-range范围内且没有被占用,否则报错。能不用固定IP就不用,毕竟容器是易变的,名字才是稳定的。

6.4 网络相关工具集锦:iptables、ip route、dnsutils

排查Docker网络问题,我常用这组工具:

# 查看docker网桥和路由 ip addr show docker0 ip route # 查看iptables规则(docker写入的规则) iptables -t nat -L -n | grep -i docker iptables -t filter -L -n | grep -i docker # 查看容器内网络状态 docker exec 容器名 ip addr docker exec 容器名 cat /etc/resolv.conf # 在容器里做DNS测试 docker exec 容器名 nslookup 目标容器名

这些命令能帮你快速定位是路由缺失、iptables规则丢了,还是DNS解析异常。我遇到过最诡异的情况是:改完宿主机网络配置后,docker0被重命名,导致已有容器全部断网。这时候重启Docker服务即可恢复,但一定要提前备份容器清单,别到时候找不全。

7. 更进一步:Docker网络的安全隔离与性能优化

到这一步,基础网络已经通了,但要把Docker网络用得漂亮,还有几个进阶点值得关注。

7.1 安全隔离:让该通的通,不该通的坚决不通

安全隔离的核心思想是“最小授权”。我一般这么设计:

  • 数据库等存储类容器放内网网络,不映射端口。
  • 中间件(Redis、消息队列)只让业务容器访问,可以用自定义网络隔离。
  • 对外提供服务的容器才用-p映射较少端口,或者放在独立的接入层网络。

还可以结合容器的network_mode参数做更严格的限制。比如某个容器只需要跑定时任务,不需要任何外网,可以设置--network none,既防止数据泄露,也降低了攻击面。

7.2 性能优化:host模式压测结论与bridge的取舍

我实测过相同业务在host模式和bridge模式下的差异。在CPU密集型、网络包较小的场景,host模式吞吐量比bridge高大概10%~20%;在网络包较大或连接数很多的场景,差距会缩小,因为瓶颈往往在应用本身或者网卡中断分配。

但host模式牺牲了灵活性。我通常只在压测、边缘网关这类组件上用host模式,常规微服务还是选bridge。另外,bridge模式下如果宿主机的iptables规则很庞杂,NAT转发可能会成为瓶颈。可以在宿主机上开启内核网络优化参数,比如调整net.core.somaxconn、net.ipv4.tcp_tw_reuse,并在容器内设置ulimit,这些优化细节虽然琐碎,但在高并发场景很管用。

7.3 多机编排与网络打通:Docker Swarm网络方案

Docker Swarm模式下,网络规划比单机多了一层服务发现和负载均衡。服务名被Swarm内置DNS自动解析,并通过VIP负载均衡分发流量。创建服务时用--network指定一个overlay网络,多个副本的服务会自动注册到该网络,前端服务通过服务名访问后端服务,无需手工维护IP列表。

如果要用Swarm自带的ingress网络暴露服务到外部,记得把服务端口发布到宿主机。docker service create --publish 8080:80 --network my-overlay --name web 某个镜像会对集群所有节点发布端口,流量会自动分发到可用副本。注意ingress网络的数据包会多一层转发,实测延迟比直连高那么一两毫秒,但对绝大多数业务无感知。

8. 一些从实际运维里总结出来的掏心话

我在多台机器上折腾Docker网络这么多年,最大的体会是:能多简单就多简单。不要一上来就堆一堆自定义网络和复杂拓扑,先把默认bridge跑通,再用自定义网络解决容器名解析,然后按需引入overlay。网络这个领域,慢慢来反而更快。

另外一个建议是:写脚本也好、写文档也好,一定把网络信息记录完整。我在公司内部就吃过亏——一套环境的网络设计没画图,结果半年后接手的人根本不知道哪个容器在哪个网络里。现在每个项目我都会顺手记一份网络拓扑描述,包含网段、网关、容器归属网络、映射端口,效果立竿见影。

还有一个小技巧,很多人不知道:docker network ls可以看到所有网络,但看不到每个网络关联的容器。想看哪个容器加入了哪些网络,用docker inspect 容器名查看NetworkSettings.Networks字段,一目了然。

如果你还在用旧版Docker,版本差异也可能带来网络行为上的细微差别。比如旧版本的自定义网络DNS有坑,新版本修复了很多。有条件就升级到更新版本,我们日常踩坑概率会低很多。

最后一个能立刻用上的建议是:用--label给容器和网络打标签。比如docker network create --label env=production prod-net,后续按标签批量管理网络会特别方便。别小看这些细节,真到了容器数量上百的时候,它们能帮你省下大量的排查时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询