你给应用容器和数据库容器建了一个自定义网络,让它们通过内网互通。数据库那边配置了放行规则,只允许应用所在网段访问。可应用一连就被拒,日志说连接被拒绝或超时。你把放行规则改来改去,就是不通。
现象
现象是"连接被拒"或"认证/网络失败",而且只在容器间通过网络访问时出现——如果你从宿主机用同样的凭据连,却完全正常。
在数据库侧(或它所在主机的防火墙日志里)能看到这样的线索:被拒绝的来源 IP,是一个容器网段的地址,比如 172.18.0.5 之类;而你的放行规则里写的,是另一个网段,比如你"以为"的 172.20.0.0/16。两者对不上,于是被规则拦下。
你翻开自己写放行规则时记的笔记,上面明明写着"容器网段是 172.20",可实际流量却来自 172.18。
根因
关键点在于:Docker 自定义网络的子网,是"分配"出来的,不是你"设定"的那个。
当你创建一个自定义 bridge 网络时,如果没有显式指定 --subnet,Docker 会从它自己的地址池里,挑一个当前没有被占用的网段分给你。不同的宿主机、不同的时间、有没有别的网络先建过,都会影响它挑到哪个。你的经验值("一般就是 172.18 吧")在下一台机器上就可能不成立。
具体地:
- 你以为的网段,可能来自"上一台机器上看到的值",或者某篇教程里的例子;
- 实际分配的网段,是 Docker 根据现有网络情况动态选的;
- 于是放行规则写的是 A 网段,实际流量来自 B 网段,规则自然不匹配。
还有一个更隐蔽的变体:容器重启或重建后,如果网络被重建过,它的子网甚至可能变。把防火墙规则硬编码成某个具体网段,本质上是在赌"它永远不变"。
解决
第一步:查出真实子网,别猜。
# 列出所有网络
docker network ls
# 查某个网络实际分配到的子网
docker network inspect <网络名> \
--format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
输出的就是这个网络真实的 CIDR,比如 172.18.0.0/16。
第二步:用这个真实值去配放行规则,而不是你记忆里的值。
第三步(更稳的做法):显式指定子网,把控制权拿回来。在 compose 里给网络写明地址段:
networks:
mynet:
driver: bridge
ipam:
config:
- subnet: 172.30.0.0/24
这样每次创建都落在这个网段,规则可以放心写死。注意两点:所选网段不能和宿主机已有网络、VPN 网段、公司内网冲突;改动后需要重建网络(docker compose down 再 up -d)。
如果只是临时想确认"容器实际用的是哪个 IP",可以:
docker compose exec app hostname -i
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <容器名>
延伸与预防
这条坑的通用教训是:凡是被系统"动态分配"的东西,都不要按经验预设。IP 地址、子网、端口、PID、容器 ID,都属于这一类。它们"看起来稳定",只是因为恰好没变过。
工程上正确的态度有两种,二选一:
- 要么显式固定(显式指定子网、固定端口映射),让它可预测;
- 要么显式查询(每次用查询命令拿当前值),别把查询结果抄进配置然后遗忘。
最差的做法是第三种:凭印象写死。这正是"上个月明明是 172.18"这类事故的来源。把"先查真实值"变成肌肉记忆,这类问题就再也不会出现。