Docker——网络配置
在 Docker 的日常使用里,网络配置可能是最让人摸不着头脑、却又绕不开的一环。镜像拉不下来、容器互相访问失败、端口映射不生效、跨主机通信断了——几乎每一个“奇怪”的故障,到最后都能追溯到网络配置层面的某个细节。
这篇文章不打算从 docker network --help 开始逐条抄文档,而是从实际使用中最常遇到的场景出发,把 Docker 网络到底做了什么、默认网络为什么不够用、端口映射背后发生了什么、跨主机通信有哪些靠谱思路、以及遇到故障该怎么一步步排查这些事讲清楚。适合那些已经会用 docker run 跑容器,但一涉及“容器A怎么访问容器B”“为什么外网访问不了我的服务”“两台机器上的容器怎么互通”就头疼的人。
1. 纸面拆解:Docker网络到底在Linux上做了什么
很多人用 Docker 用得挺熟,但问起“容器里的网络到底长什么样”,基本说不上来。这不怪谁,因为 Docker 把底层网络细节藏得很好,日常使用根本看不见。但一旦出问题,不懂底层就无从下手。
1.1 从一段“陌生关系”说起:veth 与网桥
假设你在一台 Linux 机器上执行了一个最简单的命令:
docker run -d --name web -p 8080:80 nginx这个容器起来之后,你在宿主机上会看到多出一个网桥设备,通常是docker0,同时还多了一对“虚拟网线”。
这对“虚拟网线”是 Docker 网络最核心的底层设施之一。它叫veth pair,可以理解成一根看不见的网线,一头插在宿主机虚拟网桥上,另一头插进容器的网络命名空间里。Linux 的网络命名空间(network namespace)把网络栈隔离开来,容器里的 eth0 实际上就是 veth 对的另一端。
用一张生活化的图去理解:
- 宿主机是一栋楼
- docker0 是楼里的一条走廊
- 每个容器是一间独立的房间
- veth 就是从走廊接到每个房间门口的一段管线
- 房间里的网卡(eth0)通过这段管线连到走廊,走廊再统一接出去到外部网络
这个拓扑下,同宿主机里的容器之间通信,数据包其实只经过 docker0 这个二层网桥,根本不会绕到外部交换机,所以速度非常快。
1.2 数据包默认怎么“出楼”:NAT 与 MASQUERADE
容器里的 IP 通常长这样:172.17.0.2之类的私有网段地址。这种地址在局域网里可以直接被其他设备访问吗?不行,因为网桥上的地址段是 Docker 自己起的,外部设备的路由表里根本没有这一段的条目。
为了让容器能访问外网(比如apt-get update),Docker 会在宿主机的 iptables 里加一条 MASQUERADE 规则。原理就是:容器发出的包,出宿主机网卡之前,把源 IP 悄悄改写为宿主机的出口 IP;回来的包再改回去。这个动作和家用路由器上网的原理完全一样,术语叫 SNAT。
顺带一提,很多人问“为什么我的容器能上网,但是局域网里其他机器访问不了我的容器”。答案就在这里:容器能上网靠的是 NAT,把内网包“翻译”成宿主机的 IP 送出去;但外网要主动访问容器,就得靠端口映射,也就是下面要说的 DNAT。
2. 默认三件套的隐藏代价,以及我为什么坚持用自定义网络
Docker 安装好之后,默认会创建三个网络:bridge、host、none。洗脑式的介绍里会说“bridge 是默认的,互相能通过 IP 访问”,但实际用起来坑不少。
2.1 默认 bridge 网络的致命短板:名字解析几乎等于没有
在默认的 bridge 网络里,两个容器可以通过对方的 IP 地址访问,比如172.17.0.2:3306。但问题在于:
- 容器重启后 IP 可能变了
- 代码里写死 IP 是没法接受的
- 靠
--link去弄别名,已经是老古董玩法,复杂场景下维护成本很高
关键点在于:默认 bridge 网络不提供内置的 DNS 服务,容器之间只能靠 IP 访问。而用户自定义的 bridge 网络带了 Docker 内置的 DNS 解析能力,同一个网络内的容器可以直接用容器名互相访问。
举个最简单的对比:
docker network create mynet docker run -d --name db --network mynet mysql docker run -d --name app --network mynet myapp在 mynet 里,app容器中直接用ping db、curl http://db:8080都可以通,因为 Docker 自带的 DNS server(监听在 127.0.0.11:53)会把db这个名字解析到对应容器当前的 IP。
这也是我在实际项目中一直建议——所有需要互相通信的容器,一律放到同一个用户自定义 bridge 网络里,别用默认的 bridge 网络。成本几乎没有,带来的便利却是实实在在的。
2.2 host 与 none:适合少数但明确的场景
host网络模式下容器不拥有自己的网络命名空间,直接共享宿主机的网络栈。好处是网络性能最高,没有 NAT、没有额外的转发层;坏处是容器里监听端口等于宿主机端口,容易冲突。
none模式则相当于给了容器一个有网卡但没接线的环境,完全断网。这种模式大多用于一些只跑离线计算的任务容器,或者希望让容器自己去配置复杂网络的高级场景。
有一点值得提醒:别一上来就用 host 模式去“解决”端口映射问题。虽说 host 模式免去了-p映射的麻烦,端口天然全开放,但随之而来的问题更多:端口冲突、安全风险、容器与宿主机网络栈耦合导致的排障困难。在我看来,host 模式只有在性能要求极苛刻且你非常清楚自己在做什么的时候才值得选。
3. 端口映射:-p 背后的 iptables 逻辑与常见误伤
端口映射是最常用的功能,docker run -p 8080:80 nginx谁都会敲。但“为什么会失效”“同样一条 -p 为什么有些环境不生效”这些深坑,很多人是等到线上出故障才去翻 iptables 的。
3.1 -p 参数到底做了什么
Docker 在创建容器时,如果带了-p,它会默默在宿主机 iptables 的 NAT 表里添加一条 DNAT 规则。意思是:所有发到宿主机 8080 端口的 TCP 包,目标地址和端口被改写为容器的 IP 和 80 端口。
所以外网访问路径大约是这样的:
外部设备 → 宿主机IP:8080 → iptables DNAT → 172.17.0.2:80 → 容器这里要注意一个经常被忽略的点:Docker 的端口映射只处理宿主机上 iptables 的规则,不会自动帮你向云平台的安全组或云防火墙开放端口。很多人明明容器端口映射好了,局域网里也能访问,但网上的用户访问不了,最后发现是云控制台的安全组没放行。这个坑太常见了。
3.2 一个典型的踩坑现场:iptables 规则被覆盖
我曾在一个摸拟项目X的部署环境里踩过这么一次:宿主机上跑了个 nginx 容器,映射-p 80:80,一切正常。后来运维在宿主机上装了一个带防火墙管理功能的软件包,顺手启用了它的规则管理功能,重启之后容器端口映射突然就不通了。
排查下来发现,是那个软件包在初始化时重置了 iptables 的 NAT 表规则,Docker 之前写入的 DNAT 规则没了。解决方式有两种:
- 重新创建容器,让 Docker 重新写入规则
- 手动把规则加回去(不推荐,因为 Docker 的一些管理逻辑会与手动规则冲突)
这个经历后来让我养成一个习惯:宿主机上启用任何会修改 iptables 的软件之前,先备份规则:
iptables-save > /tmp/iptables-backup-$(date +%F-%T).rules如果真出问题,恢复也很简单:
iptables-restore < /tmp/iptables-backup-2025-xx-xx.rules3.3 端口冲突的另一种隐藏形式
-p 8080:80报错“port is already allocated”算是最直白的冲突。还有一种隐藏冲突:宿主机上某个进程监听在0.0.0.0:8080,你再用-p 127.0.0.1:8080:80去映射,照样会失败或出现诡异行为。
想排查端口到底被谁占用:
ss -tlnp | grep 8080如果输出显示127.0.0.1:8080被某进程监听,而 Docker 想绑定0.0.0.0:8080,就会出现半通不通的现象。这种问题光看 Docker 日志是不太容易发现的,还是得回到系统层面去看。
4. 跨主机通信:从桥接现实的四种常用打法
单机场景,自定义 bridge 网络基本够用。但一旦进入集群化部署,比如两台机器上的容器需要互相访问,问题就复杂起来了。
4.1 方案一:宿主机端口映射的“土办法”
最简单粗暴的跨主机通信方式:容器 A 在宿主机 1 映射一个端口,容器 B 在宿主机 2 通过宿主机1的IP:端口访问。
好处是简单,不需要额外组件;坏处同样明显:服务发现要靠人工维护端口映射表、端口资源浪费、所有流量都绕了一圈 NAT,性能和延迟都不理想。适合临时联调、测试环境,不适合生产。
4.2 方案二:Docker 内置的 overlay 网络
Docker Swarm 模式下可以创建 overlay 网络,它在底层用 VXLAN 隧道把多台宿主机上的容器连接成同一个虚拟二层网络。容器拿到的 IP 是跨主机统一的网段,容器名解析也能跨主机生效。
使用前需要先初始化 Swarm 集群:
docker swarm init --advertise-addr 宿主机IP docker network create -d overlay --attachable my-overlay不是 Swarm 服务也能用 overlay,加--attachable参数就能让普通容器加入。这个方案适合小规模集群、要求快速搞定且不想引入太多外部组件的情况。
overlay 网络有个性能上的天然代价:每个跨主机数据包都要做 VXLAN 封装和解封装,CPU 开销比 bridge 高。机器性能强的话还好,弱的话延迟会明显增加。
4.3 方案三:Macvlan 直接给容器分配物理网络 IP
Macvlan 的方案是让容器直接使用宿主机所在局域网的真实 IP。每个容器会获得一个物理网段的地址,外部设备可以直接访问容器,不需要 NAT,不存在端口映射的烦恼。
配置示例:
docker network create -d macvlan \ --subnet=192.168.1.0/24 \ --gateway=192.168.1.1 \ -o parent=eth0 \ my-macvlan这个方案特别适合“容器需要被局域网设备当成普通主机访问”的场景,比如智能家居控制系统、局域网内的服务发现等。缺点是:要求局域网 IP 资源充足,而且宿主机和容器之间的通信会比较别扭(默认宿主机无法直接用容器 IP 访问容器),需要额外加一个同网段的 Macvlan 接口才能解决。
4.4 方案四:部署一套 CNI 插件(Kubernetes 场景)
如果已经在用 Kubernetes,跨主机网络基本不用 Docker 层面操心,而是由 CNI 插件负责,比如 Flannel、Calico 等。这类工具会处理跨节点 Pod 网络的路由、策略和安全,是生产环境的首选。
在纯 Docker 环境下,我个人的经验是:小规模集群(三五台以内)用 overlay 或 Macvlan 都行;规模再大一点,还是尽早迁移到编排平台比较省心,否则网络配置的人工维护成本会指数级上升。
5. 一次线上的网络故障排查链路:以“两个容器互相访问不稳定”为例
工具和方案介绍再多,不如完整走一遍排障思路。下面以一个很常见的故障案例来演示排查过程,这个案例我起名叫“模拟微服务联调环境”。
5.1 故障背景与表象
某套系统由两个 Docker 容器组成:api-service和redis-cache。两个容器放在同一个自定义网络backend-net里。起初运行正常,某天运维重启了宿主机,之后api-service访问redis-cache时频繁出现间歇性超时,且错误信息飘忽不定。
5.2 第一步:确认网络拓扑没有变化
先进自定义网络里看有哪些容器:
docker network inspect backend-net输出里能看到api-service和redis-cache的 IP。重点看两个点:
- 容器的 IP 是否和重启前一致(大概率会变)
- 容器是否还都附着在这个网络里
这次排查中发现两个容器都正常附着在网络里,IP 变了但分配合理,问题不在拓扑变化。
5.3 第二步:验证容器间连通性
进入api-service容器内部:
docker exec -it api-service bash ping redis-cache一般会直接说需要用 redis-cli 去测 TCP 连通性。此时用:
redis-cli -h redis-cache -p 6379 ping实测发现部分请求能通,部分超时。这说明基础连通性时好时坏,问题不是 DNS 解析失败,而更像是链路层丢了包。
5.4 第三步:识别“隐藏的规则冲突”
回到宿主机查看 iptables 里 Docker 相关链的规则:
iptables -t nat -L POSTROUTING -n -v发现有一条规则和 redis-cache 容器的源地址区间冲突。原来宿主机上的另一个程序(比如某些监控工具自带的 iptables 规则)也针对同一网段写了一条 DNAT 规则,两条规则优先级互相干扰,导致部分转发异常。
排查到这一步,处理方案就明确了:禁用或调整第三方程序的规则。把冲突规则删除后,容器间访问恢复稳定。
5.4 复盘:把排障链路沉淀为固定套路
这次踩坑之后,我把容器的网络排障流程固定成了一个基本套路:
docker network inspect确认容器都在正确的网络里,IP 是否异常docker exec进容器,先用ping测 ICMP,再用实际业务端口测 TCP- 在宿主机上用
iptables -t nat -L -n -v检查规则是否有冲突 - 用
tcpdump -i docker0看数据包是否到了网桥这一层 - 检查宿主机的防火强工具(firewalld、ufw 等)是否拦截了 Docker 的转发
这个套路我后来用过很多次,基本能覆盖九成以上的“容器间网络不稳定”类问题。
6. 收尾:几个我踩过坑之后才养成的网络配置习惯
最后分享几个我在实际运维中总结出来的习惯。不一定每条都能让你立刻“变强”,但照着做能省掉很多莫名其妙的排查时间。
第一,用户自定义网络务必显式指定子网。之前为了偷懒直接用docker network create mynet,靠 Docker 自动分配网段。结果不同环境里网段不一样,有些网段和公司内部网络冲突,导致路由异常。现在所有网络创建都会显式带子网:
docker network create \ --subnet=172.22.0.0/16 \ --gateway=172.22.0.1 \ mynet第二,除非有明确理由,不要手动修改 Docker 创建的 iptables 规则,也不要在宿主机上跑额外防火墙策略去管 Docker 的网段。Docker 对 iptables 有自己的一套管理逻辑,手动规则很容易被 Docker 重置逻辑清掉,或者反过来干扰 Docker 的转发。
第三,生产环境尽量用容器名而不是 IP 去配置依赖关系。自定义网络里的 DNS 解析就是为了解决“IP 会变”的问题。你要是还在代码里写死容器 IP,趁早改成配置中心或者环境变量注入。
Docker 网络的内容说多不多,说少不少。来回折腾过几次之后你就会发现,所有问题的核心其实就是两层:理解网桥和 veth 的本地通信模型,以及理解 NAT 和 iptables 的转发模型。这两块通了,绝大多数配置和排障都能自己搞定,而不是到处搜“为什么我的容器访问不了另一个容器”。