☰
Docker网络实战:五种模式、端口映射、跨主机组网与分层排错
2026/10/26 5:48:33 网站建设 项目流程

容器里的服务自己跑得好好的,健康检查也过,可同宿主机上另一个容器拿服务名去连,死活连不上;换成一个固定 IP 立刻通。这种现场我碰到过不止一次,第一次遇到时足足查了两个小时,最后发现是两个容器根本不在同一个网络里——名字解析走的是 Docker 内嵌的 DNS,而默认的 bridge 网络压根不提供这个能力。Docker 网络这块知识,文档上写的都是参数说明,真正让人卡住的往往是参数背后那条包从容器出来、穿过网桥、被规则改写、最后落到宿主网卡上的完整链路。这篇东西就是把我这些年攒下来的 Docker 网络实操经验摊开讲:默认网络到底怎么拼出来的,五种网络模式各自的边界在哪,-p 参数究竟动了几张表,跨主机组网为什么总在 MTU 上翻车,以及排错时该怎么一层一层往下剥。不管你是刚把第一个容器跑起来的新手,还是已经在维护多服务拓扑的老手,下面这些内容应该都能对上你踩过的某个坑。

1. 一个连不上的容器,把 Docker 网络的组成拆开了

1.1 容器网络由哪几块拼成

很多人对 Docker 网络的第一印象就是docker network ls里那几个名字,觉得网络就是一个配置对象。实际上容器能通网,靠的是四样东西叠在一起:一是network namespace,它给容器提供了一套独立的网卡、路由表、iptables 规则和端口空间;二是veth pair,一对虚拟网线,一头插在容器的 namespace 里,另一头插在宿主的 namespace 里;三是网桥,宿主上的 docker0 或者自定义网桥,负责把这些虚拟网线连成一张二层网络;四是iptables 规则,负责地址转换和转发放行。四者缺一,容器就是个孤岛。

理解这个分层很关键,因为它决定了排错的方向。容器 ping 不通外网,问题可能在路由表(第三层),可能在网桥转发(第二层),也可能在 NAT 规则(iptables),还可能是宿主内核的ip_forward没开。你在容器里ping一个域名失败,看见的是同一个现象,但根因差得远。所以我自己养成一个习惯:遇到网络问题,先在宿主机上ip link看一眼 veth 是否存在,再docker network inspect看容器挂在哪张网桥上,最后iptables -t nat -L看规则,三分钟能定位大半。

顺带说一个容易忽略的点:veth pair 是成对存在的,容器一停,宿主上对应那根 veth 也就消失了。所以你不要用ip link里 veth 的数量去长期统计容器数,那个数字是实时变化的,脚本里依赖它会出问题。

1.2 docker0 网桥和 veth pair 长什么样

装完 Docker 之后,宿主机上一定会多出一块叫docker0的网卡,它默认拿到172.17.0.1/16,状态是 UP 但通常没有物理上联。这块网卡就是默认 bridge 网络的网关。容器启动时,Docker 会创建一对 veth,容器内那端通常叫eth0,宿主那端叫veth加一串随机字符,然后把宿主端 attach 到 docker0 上。

你在容器里ip addr看到eth0的地址是172.17.0.2/16,在宿主上ip link show能看到vethxxxx@if3这种形式,@if3后面的数字就是容器内那端的 interface index。这个对应关系在抓包时特别有用:你想看某个容器的流量,就在宿主上tcpdump -i vethxxxx,或者在 docker0 上抓全部。我排一个丢包问题时就是这么干的,先在 docker0 上抓,发现包根本没出来,往回一查是容器内的路由表把目标网段指向了错误的网关。

还有一个细节:默认 bridge 的容器 IP 是从172.17.0.0/16里顺序分配的,容器删了地址可能被别的容器复用。如果你在配置里硬编码了容器 IP,重启之后大概率出事。这也是我后面强烈推荐自定义网络的原因之一——自定义网络虽然也会复用地址,但至少你能通过名称解析绕开 IP 硬编码这个坑。

1.3 出网这一步:NAT 链上的顺序

容器访问外网,包先到 docker0,宿主内核发现目标不是本地网段,查路由表决定从物理网卡出去,然后进POSTROUTING链做源地址转换,把源 IP 从172.17.0.2改成宿主网卡的 IP,回包再反向转换回来。这条 MASQUERADE 规则在iptables -t nat -L POSTROUTING里能看到,条件是源地址属于172.17.0.0/16且出口不是 docker0。

问题经常出在FORWARD链上。有些系统的防火墙默认策略是 DROP,Docker 启动时会往里插自己的规则,但如果规则顺序被别的工具打乱,或者有人手动清过链,容器就出不了网,现象是容器内 ping 网关通、ping 外网不通。这时候你去看iptables -L FORWARD -n -v,会看到 Docker 相关的链没有被引用。修复方式不是暴力iptables -F,而是重启 Docker 服务让它重新插入规则,或者在防火墙配置里显式放行 docker0 的转发。

我个人的经验是:永远不要在装了 Docker 的机器上随手清 iptables。那些规则不是垃圾,是容器通网的命脉。要定制规则,走DOCKER-USER链,这条链是 Docker 专门留给用户的前置钩子,位置在 Docker 自己的规则之前,改它不会被容器重启冲掉。

2. 五种默认网络模式:它们不是选项列表,是五个答案

2.1 bridge、host、none、container 的边界

docker network ls里默认能看到 bridge、host、none 三个,第四个 container 模式不在列表里,是通过--network container:xxx引用的。这四个加自定义网络,构成了容器网络的全部基础形态。它们的区别不在于"哪个更好",而在于"你要解决什么问题"。

bridge 是默认值,容器有独立网络栈,通过 NAT 出网,需要端口映射才能被外部访问。host 模式下容器直接共享宿主的网络命名空间,容器里监听 80 就是宿主监听 80,不需要-p,也不会有 NAT 开销。none 模式只给容器一个 lo,完全没有对外网卡。container 模式让新容器复用另一个已存在容器的网络栈,两者共享 IP 和端口空间。

模式独立网络栈需要端口映射典型用途
bridge(默认)是是绝大多数普通服务
自定义 bridge是是多容器协作,需要名称解析
host否否高性能网络、需要拿到真实客户端 IP
none是(仅 lo)不适用纯计算任务、离线批处理
container否(共享)否边车容器、共享网络栈的辅助进程

这张表看着简单,实际选型时的判断依据只有两条:这个容器需不需要独立 IP,以及它需不需要和别的容器按名字互相找。第一条决定用不用 host,第二条决定用不用自定义网络。默认 bridge 在这两个问题上都是"都不太行"的答案,所以我几乎不在正式环境里用它。

2.2 host 模式被低估的地方与被高估的地方

host 模式最实在的好处是省掉了一层 NAT。高并发短连接的场景下,NAT 带来的 conntrack 表压力和端口耗尽风险是真实存在的,我之前维护的一个采集服务,QPS 上去之后经常在 conntrack 上报警,换成 host 模式后这个现象明显缓解。另一个好处是容器里看到的源 IP 就是真实客户端 IP,不用在应用层去解析X-Forwarded-For这类头部。

但 host 模式的代价也很明确:容器不再有独立 IP,端口直接和宿主共享,两个容器都想监听 8080 就必然冲突。它还会破坏容器"可随意迁移"的前提——如果宿主上 80 端口已经被别的服务占了,这个容器就起不来。更要命的是可观测性变差,你在宿主上ss -lntp看到的进程归属会变得混乱,排查端口占用时得额外区分是宿主进程还是容器进程。

还有一个常被误解的点:host 模式下-p参数不是报错,而是被静默忽略并给一条 warning。我就见过有人写了-p 8080:80 --network host,然后发现服务监听的其实是 80,-p完全没生效。这种"参数写了但不起作用"的情况最容易误导排查方向。

我的判断标准很直接:需要拿到真实源 IP、或者对网络性能极其敏感、且端口不会冲突的服务,用 host;其余一律用自定义 bridge。中间地带的服务,宁可多一层 NAT,也别为了省那点开销牺牲部署灵活性。

2.3 none 与 container 模式的真实用途

none 模式听起来很边缘,其实在安全敏感的场景里挺有用。给容器一个完全隔离的网络环境,只留 lo,意味着它连 DNS 都查不了,任何外联都出不去。做数据处理的批任务、跑纯计算的模型推理、执行一次性的脚本,用 none 模式能从网络层面掐断意外外联的可能。我自己在做一些需要严格隔离的数据清洗任务时就会用--network none,跑完把结果通过挂载卷写出去,整个过程不碰网络。

container 模式的价值在于共享网络栈。最典型的用法是边车:主容器跑业务,边车容器跑一个本地转发或者采集程序,两者共享同一个网络命名空间,边车可以直接监听127.0.0.1上的端口,主容器访问127.0.0.1就等同于访问边车。这种模式下两个容器连 IP 都一样,端口自然也不能冲突。

不过 container 模式有个隐蔽的坑:网络栈的生命周期跟着被引用的那个容器走。如果主容器重启,网络命名空间会重建,边车容器虽然还在跑,但它引用的那块网络栈已经变了,可能出现"进程活着但网络断了"的诡异状态。所以这个模式我不建议用在长时间运行的服务上,用在短生命周期的辅助任务里更合适。

3. 自定义 bridge 网络:正式环境绕不开它的三个理由

3.1 内嵌 DNS 是真正的分水岭

默认 bridge 网络最让人难受的地方是容器之间只能用 IP 互访。早年有--link参数能做名称关联,但那个机制本质是往容器的 hosts 文件里写死记录,容器重启 IP 一变就失效,而且 link 是单向的,A link B 之后 B 看不见 A。这个参数现在已经被标记为遗留特性,新项目不该再用。

自定义 bridge 网络解决这个问题的方式是内嵌一个 DNS 解析器,监听在127.0.0.11:53。容器内的/etc/resolv.conf里 nameserver 会指向这个地址,Docker 通过 iptables 规则把 53 端口的查询重定向到内嵌 DNS。这个 DNS 认识同一网络里所有容器的名字、服务名和--network-alias指定的别名,还负责把不属于容器名的查询转发给宿主配置的上游 DNS。

这个机制带来的直接好处是:容器名就是主机名。你docker run --name mysql-db --network appnet,同网络里另一个容器ping mysql-db就能通,不用关心它实际分到的是172.18.0.2还是172.18.0.7。在 compose 里更是如此,服务名自动成为可解析的主机名,这也是为什么 compose 项目里写depends_on加服务名当连接串能直接跑通。

有个细节值得记一下:内嵌 DNS 只在自定义网络里生效,默认 bridge 上的容器用的是宿主机的 DNS 配置,解析不了容器名。这就是开头那个"换 IP 就通"问题的根因。判断方法很简单,进容器看一眼/etc/resolv.conf,如果 nameserver 是127.0.0.11,说明在自定义网络里;如果是宿主那几个 DNS 地址,那就是默认 bridge。

3.2 网络隔离与容器生命周期

自定义网络天然带隔离。不同自定义 bridge 网络的容器之间默认完全不通,哪怕它们在同一个宿主上、IP 段相邻。这个特性在做多租户或者多环境共存时特别有用:把前端、后端、数据库划到三张网络里,只有需要通信的两张网络才把容器同时接进去,其余的组合天然隔离,比写一堆 iptables 规则靠谱得多。

生命周期这块也值得说。自定义网络是独立对象,容器删了网络还在,重新起容器挂上去,之前接入的其它容器不受影响。默认 bridge 虽然也是长期存在的,但你没法给它加参数,也没法控制它的子网,更没法在 compose 里声明。而自定义网络可以在创建时指定子网、网关、IP 范围、MTU,甚至指定--internal让它完全不能出网。

--internal这个参数我要单独提一句,它创建出来的网络没有默认路由,容器只能和同网络的容器通信,出不了外网。做"只有数据库层能互相访问、绝对不能连外网"这种架构时,直接建一个 internal 网络把数据库容器塞进去,比事后配防火墙简单得多,而且不会因为规则疏漏被绕过。

3.3 从零创建一个自定义网络并验证

操作本身不复杂,但每一步都有验证手段,我习惯一步步确认而不是一把梭。

# 创建带明确子网的自定义网络 docker network create \ --driver bridge \ --subnet 172.28.0.0/16 \ --gateway 172.28.0.1 \ --opt com.docker.network.bridge.name=br-app \ appnet

这里指定com.docker.network.bridge.name是为了让宿主上的网桥有个可读的名字。不指定的话,宿主上会多出一块br-加 12 位随机字符的网卡,抓包和排查时你得先docker network inspect去查它对应哪张网络,很麻烦。指定成br-app之后,tcpdump -i br-app就能直接抓这张网络的流量。

# 起两个容器接入同一网络 docker run -d --name svc-a --network appnet alpine sleep 3600 docker run -d --name svc-b --network appnet alpine sleep 3600 # 验证名称解析和连通性 docker exec svc-a ping -c 2 svc-b docker exec svc-a nslookup svc-b # 确认容器挂载情况 docker network inspect appnet --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'

docker network inspect的输出里有个Containers字段,列出所有接入的容器和它们的地址,这个比一个个docker inspect快得多。我平时排查"某个容器到底在不在这个网络里",就用这条命令,一眼就能看出来。

还有一个容易踩的点:容器运行中不能直接切换网络,只能用docker network connect追加一张网卡。一个容器可以同时接入多张自定义网络,容器内的eth0、eth1分别对应不同的网络。这个能力在需要跨网络访问的场景里很有用,比如让一个网关容器同时接前端网络和后端网络,但要注意接多张网络之后容器的默认路由会指向第一张网络,出网走哪条链路需要确认清楚。

4. 端口映射的完整链路:-p 到底改了几张表

4.1 从参数到 iptables 规则的对应

写-p 8080:80的时候,大部分人的心智模型是"宿主 8080 转发到容器 80",这个理解没错但太粗。实际发生的事情分几步:Docker 先在nat表的DOCKER链里插一条 DNAT 规则,把目标为宿主 8080 的包改写成容器 IP 的 80 端口;同时在filter表的DOCKER链里插一条放行规则;另外还会启动一个用户态转发进程监听宿主 8080,作为兜底路径。

你可以自己验证:

# 查看 DNAT 规则 sudo iptables -t nat -nL DOCKER # 查看转发放行规则 sudo iptables -nL DOCKER # 看用户的转发进程 sudo ss -lntp | grep 8080

iptables -t nat -nL DOCKER的输出里能看到类似DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.28.0.2:80的规则。这条规则解释了为什么容器重启换了 IP 之后端口映射还能继续工作——Docker 会同步更新这条规则。但这个同步依赖 Docker 守护进程正常工作,如果有别的工具在管理 iptables,两边打架就会出现"规则还在但指向了不存在的地址"的情况,现象是连接被拒绝或者超时。

4.2 用户态转发进程与回环地址的处理

默认情况下 Docker 会起一个用户态转发进程。很多人不理解为什么有了 iptables 还需要它,原因在于nat表的 DNAT 只在 PREROUTING 和 OUTPUT 链生效,本机进程访问127.0.0.1:8080时走的是 OUTPUT 链,而 Docker 为了兼容这种情况,会额外加一条针对回环来源的规则,同时保留用户态进程作为兜底。

这个进程有时候会带来麻烦。比如你把 Docker 的转发进程关掉(配置里"userland-proxy": false),性能会好一点,但某些特殊网络配置下从本机访问映射端口可能就不通了。反过来,如果你在宿主上用ss看到一堆转发进程占着端口,而对应容器已经删了,那就是规则和进程没清理干净,需要重启 Docker 服务。

端口绑定地址也值得注意。-p 8080:80默认绑定所有网卡,包括公网网卡;-p 127.0.0.1:8080:80只绑定回环。后者在只允许本机其他服务访问、不允许外部直连的场景里应该成为默认选择。我见过不少配置直接写-p 3306:3306把数据库暴露到公网,然后靠数据库密码强度硬扛,这个习惯得改。

4.3 端口冲突与 DOCKER-USER 链的正确用法

端口冲突的报错信息很明确:Bind for 0.0.0.0:8080 failed: port is already allocated。但有时候端口明明没被别的容器占用,还是报这个错,那多半是宿主的其它进程占了,或者之前删掉的容器残留了转发进程。查法就是ss -lntp | grep 8080,看清楚是谁占的。

如果要限制谁能访问映射出来的端口,别去改DOCKER链,那是 Docker 自己维护的,重启动就没了。正确的位置是DOCKER-USER链,Docker 会在FORWARD链的最前面跳转到它,你在这里加的规则对所有容器转发流量生效,而且容器重启不会覆盖。典型用法是只允许某个网段访问某个端口:

# 只允许 10.10.0.0/16 访问宿主 3306 映射端口 sudo iptables -I DOCKER-USER -p tcp --dport 3306 -s 10.10.0.0/16 -j RETURN sudo iptables -A DOCKER-USER -p tcp --dport 3306 -j DROP

注意这里第一条用-I插到最前面,第二条用-A追加到末尾,顺序不能反。RETURN表示放行并结束这条链的匹配,走回 Docker 自己的规则;DROP则直接丢弃。这个顺序搞反了会把所有请求都干掉,包括白名单里的。改完记得用iptables-save把规则持久化,不然重启就丢。

5. 跨主机组网:overlay 与 macvlan 的适用边界

5.1 VXLAN 封装带来的 MTU 账

单机上的网络问题都好办,一旦容器要跨主机通信,复杂度立刻上一个台阶。overlay 网络是 Docker 原生支持的跨主机方案,原理是把容器的二层帧封装进 VXLAN,通过宿主之间的 UDP 隧道传输。容器以为自己在同一个二层网络里,实际上包在宿主之间被套了一层壳。

这层壳是有成本的。VXLAN 头部加外层以太头、IP 头、UDP 头,一共约 50 字节。宿主网卡 MTU 是 1500 的话,隧道里能过的大包就被压到 1450;如果容器网卡还按 1500 配,一个 1500 字节的包封装后变成 1550,超过物理链路 MTU,就会被丢弃或者分片。表现出来就是:小请求一切正常,一旦返回大数据量就卡死,TCP 连接建得起来但传不完。

这个坑我踩过一次,排查了很久。当时一个接口在小数据量下完全正常,一旦查询结果超过几 KB 就超时。最后用ping -M do -s 1472一步步试出来的:1472 加 28 字节头部正好 1500,本地通;换成 overlay 网络后 1472 就不通了,得降到 1422。结论是 MTU 没配对。

解决办法是在创建网络时显式指定 MTU:

docker network create \ --driver overlay \ --opt com.docker.network.driver.mtu=1450 \ myoverlay

或者直接在 Docker 守护进程配置里全局设"mtu": 1450。别指望自动探测,Docker 不会帮你做路径 MTU 发现,这个值必须自己算清楚。

5.2 macvlan 让容器直接拿到二层地址

macvlan 是另一条路:不封包,直接给容器分配一个和宿主同网段的 IP,每个容器有自己独立的 MAC 地址,在物理交换机看来就是一台独立的机器。好处是性能几乎无损,容器可以直接被同网段其它设备访问,不需要端口映射,也不需要 NAT。

代价是配置要求高。宿主网卡必须支持混杂模式,很多虚拟化平台的默认网卡不支持;容器的 IP 需要从物理网段里划一块出来,还得确保不和 DHCP 池冲突;最麻烦的是宿主和容器之间默认不通,因为 macvlan 的设计里同一张网卡上的父子接口不能直接通信,需要在宿主上再建一个 macvlan 子接口才能打通。

# 创建 macvlan 网络 docker network create -d macvlan \ --subnet=192.168.10.0/24 \ --gateway=192.168.10.1 \ --ip-range=192.168.10.128/25 \ -o parent=eth0 \ macnet

这里的--ip-range很关键,它把容器的地址限制在网段的后半段,避免和物理机、DHCP 分配冲突。-o parent=eth0指定挂在哪块物理网卡上。这套配置在机房环境里效果很好,但在云主机上经常行不通,因为云平台的网卡通常不允许随意改 MAC 或者开启混杂模式。

5.3 三种跨主机方案的选型对照

方案性能开销配置复杂度适用场景主要限制
overlay(VXLAN)中等,有封装开销中,需要管理集群多主机服务互访,动态扩缩容MTU 需手动对齐,隧道端口要放通
macvlan很低,几乎无损高,依赖底层网络需要容器直接暴露在物理网段宿主与容器通信需额外配置
host 模式 + 外部路由最低中,需要网络侧配合高性能、固定拓扑端口冲突,失去隔离性

选型的核心判断是:你需要的是逻辑上的一张网,还是物理上的一台机器。前者用 overlay,容器之间按名字互相找,扩缩容无感;后者用 macvlan,容器在网络设备眼里就是一台真实主机。两者都不完美,实际项目里我更多是 overlay 打底,个别对延迟极敏感的服务单独用 macvlan 或者 host 模式。

6. 排错链路:从名字解析到丢包的分层定位

6.1 第一层:名字到底能不能解析

网络不通,先分清楚是"解析不了"还是"连不上",这两个问题的排查路径完全不同。进容器先看三样东西:

docker exec -it svc-a sh -c 'cat /etc/resolv.conf; echo ---; cat /etc/hosts; echo ---; nslookup svc-b'

如果/etc/resolv.conf里的 nameserver 不是127.0.0.11,说明容器不在自定义网络里,容器名解析必然失败,这时候要么换网络,要么加--add-host手动写 hosts(不推荐,IP 一变就废)。如果 nameserver 正确但nslookup查不到,那要看两个容器是否真在同一张网络上,docker network inspect一查就知道。

还有一种情况是解析出来的地址不对。比如容器名和某个外部域名撞了,内嵌 DNS 优先返回容器地址,导致本该访问外部服务的请求打到了容器上。这种问题极难发现,现象是"偶尔连到奇怪的服务"。规避办法是容器命名时加项目前缀,别用redis、mysql这种通用词当容器名。

/etc/hosts里 Docker 也会写几条记录,包括容器自己的主机名。如果你手动改过 hosts,注意这些记录在容器重启后会被重写,任何手改都不是持久方案。

6.2 第二层:包出不出去,路由对不对

解析通了还连不上,下一步看路由和连通性。在容器里依次做这几件事:

# 看接口和地址 ip addr show # 看路由表,确认默认网关 ip route show # 测网关通不通 ping -c 2 172.28.0.1 # 测目标端口通不通(不依赖应用层协议) nc -zv svc-b 6379

ip route的输出能看出很多问题。正常情况下应该有一条default via 172.28.0.1 dev eth0。如果没有默认路由,容器出不了网;如果默认路由指向了错误的网关,包发出去就石沉大海。这种情况在多网络接入的容器上特别常见——接了eth0和eth1两张网络,默认路由只指向其中一张,访问另一张网络里的服务就得靠静态路由或者明确指定出口。

nc -zv比ping更有价值,因为很多环境禁了 ICMP 但 TCP 是通的。端口通说明链路层和传输层没问题,问题在应用;端口不通就往网络层查。这个习惯帮我省了很多时间,别一上来就ping,先测 TCP。

宿主机侧也别忽略。tcpdump -i br-app -nn在自定义网桥的网卡上抓包,能看到容器发出来的原始帧;tcpdump -i any port 6379能看全链路。如果容器发出来了但目标没收到,问题在转发规则;如果容器压根没发,问题在容器内部。

6.3 第三层:大包黑洞与 conntrack 表满

前面两层都通,但业务还是偶发失败,这时候要看两个隐蔽问题。

大包黑洞:表现是小请求正常,大响应卡死。判断方法是在容器里用不同大小的包做不分片测试:

# 1472 + 28 = 1500,标准以太网 MTU ping -M do -s 1472 -c 2 目标地址 # overlay 环境下应该能通的最大值 ping -M do -s 1422 -c 2 目标地址

如果小包通、大包不通,几乎可以确定是 MTU 问题。修复方式是把容器网络的 MTU 调成和隧道匹配的值,或者让应用层控制单次传输的数据量。TCP 有 MSS 协商机制,理论上能自适应,但如果中间有设备丢弃了 ICMP 的"需要分片"消息,协商就会失败。

conntrack 表满:现象是服务运行一段时间后,新建连接开始随机失败,内核日志里出现nf_conntrack: table full, dropping packet。查看当前用量:

cat /proc/sys/net/netfilter/nf_conntrack_count cat /proc/sys/net/netfilter/nf_conntrack_max

高并发短连接的场景很容易把表打满。临时调大是sysctl -w net.netfilter.nf_conntrack_max=1048576,同时把nf_conntrack_tcp_timeout_time_established从默认的 5 天调小。但根治还是从架构上减少不必要的连接数,比如开启连接池、把高频服务改到 host 模式绕开 NAT。

7. 一套贴近业务的 compose 网络划分模板

7.1 分区的思路:把网络当权限边界用

很多人写 compose 的时候把服务全塞一个默认网络里,省事,但等于放弃了网络层面的隔离。我更倾向把网络当权限边界来用:前端网络只放对外暴露的服务和接入层,后端网络放业务服务和缓存,数据网络放数据库,每条边界上只放必须跨界的那个容器。

这样做的直接收益是爆炸半径变小。万一某个前端服务被攻破,它所在的网络里只有接入层组件,碰不到数据库。而且这种隔离不需要额外配置防火墙规则,靠 Docker 网络默认不通的特性就能实现。

划分粒度也不用太细,一个中等规模的系统三到四张网络足够。网络太多反而增加维护成本,每次加服务都得想清楚该挂哪张网。我的习惯是按"是否需要互相通信"分组,而不是按"技术栈"分组——同一个业务域的服务放一张网络,哪怕它们是不同语言写的。

7.2 完整的 compose 网络配置

services: gateway: image: nginx:alpine networks: - frontend - backend ports: - "127.0.0.1:8080:80" api: image: myapp/api:latest networks: - backend - data environment: DB_HOST: db CACHE_HOST: cache cache: image: redis:7-alpine networks: - backend command: ["redis-server", "--maxmemory", "256mb"] db: image: postgres:16-alpine networks: - data volumes: - pgdata:/var/lib/postgresql/data networks: frontend: driver: bridge ipam: config: - subnet: 172.30.0.0/24 backend: driver: bridge ipam: config: - subnet: 172.30.1.0/24 data: driver: bridge internal: true ipam: config: - subnet: 172.30.2.0/24 volumes: pgdata:

这份配置有几个设计意图值得说明。gateway同时挂frontend和backend,它是唯一能同时看见两边的容器,负责转发;api挂backend和data,能访问数据库;cache只挂backend,数据库访问不到它;db单独在data网络上,而且data网络声明了internal: true,意味着这个网络里的容器没有默认路由,出不了外网。

internal: true这一条是我最看重的。数据库容器最常见的风险不是被外部直接攻击,而是容器内的进程被诱导去访问外部地址(比如拉取恶意依赖、回连某个地址)。把网络封死之后,这类路径直接断掉。需要给数据库做备份之类的外联操作时,临时用docker network connect接一张外网网络,操作完再断开,比长期开着口子好得多。

子网显式指定也很有必要。不指定的话 Docker 会自己从172.18.0.0/16开始分配,宿主机上多跑几个项目就可能撞上网段冲突,尤其是和公司内网或者家庭网络的网段重叠时,现象是容器内访问某个内网地址莫名其妙地打到了容器上。显式规划成172.30.x.0/24这种不常用的段,能省掉很多后续麻烦。

compose 启动之后,容器之间的解析直接用服务名。注意depends_on只保证启动顺序,不保证服务就绪,连接还得在应用层做重试。这一点跟网络配置无关但经常一起出问题,我索性在这里一并提一句。

最后补一个实用技巧:排查 compose 网络问题时,docker compose exec api getent hosts db比nslookup更接近应用的真实解析路径,因为很多精简镜像里根本没装nslookup,但getent基本都有。这个命令能同时告诉你解析结果和走的是哪条路径,排查 DNS 问题时比装一堆工具快。

我在实际使用中最大的体会是,Docker 网络的复杂度不在配置本身,而在于它把好几层抽象的边界藏起来了——容器里的localhost、宿主的localhost、网桥上的地址、物理网卡的地址,四者关系不清就会陷入反复试错的循环。把这四层在心里画成一张图,遇到问题先定位在哪一层,绝大多数故障几分钟内就能收敛。

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

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

立即咨询