中文 English

Docker 容器太多导致网段冲突:把自动分配网络统一迁回 172 私有段的完整记录

发布时间: 2026-04-13
docker network nas portainer dockhand ipam synology

这篇文章记录的是一次很典型、也很容易被忽略的 Docker 网络故障:容器越来越多,Docker 自动分配出来的 user-defined bridge 网段,最后落进了 192.168.x.x,并且和家里的真实局域网发生了冲突。问题本身不算复杂,但它有一个很讨厌的特点:它不是那种“容器起不来”或者“端口没映射”的显性错误,而是把整个家庭网络拖进一个很别扭的半瘫痪状态。

当时在我的 NAS 上,家里部分设备开始出现访问异常。表面上看,像是某个设备离线,或者某个交换机、AP、路由器出了毛病;但我沿着 Docker 的网络一路排查下去后,最终确认,真正的根因不是单个容器坏了,而是 Docker 网络池的自动分配策略,已经把局域网挤到了一个危险的位置。

先说结论

这次的处理原则只有一条:Docker 以后只允许自动使用 172.16.0.0/12 这类私有网段,不再使用 192.168.*,也尽量不碰 10.*

这不是为了好看,而是为了稳定。家里真实网段本身也是 192.168 这一类地址,如果 Docker 再往 192.168.* 里塞 user-defined network,哪怕它们理论上是“容器内部网络”,实际运行时也很容易和宿主机路由、DNS、网关、某些跨网段访问规则撞在一起。更糟的是,Portainer、Dockhand 这类工具在不断创建新 stack、新 network 的时候,会持续消耗 Docker 的默认地址池;如果不提前约束,等你发现的时候,已经不只是一个容器的问题,而是整个网络平面开始变得不可预测。

这次我在 NAS 上做了三件事:

  1. 把 Docker 的默认地址池改成 172.16.0.0/12size=20
  2. 把现有的所有 192.168.* Docker 网络迁移到 172.16.* 私有网段。
  3. 顺手把之前那个已经飘到 172.x 非标准边界的网络也拉回到 RFC1918 的私有范围里。

最后的结果很简单:

问题是怎么暴露出来的

最开始我并没有把它当成 Docker 的问题。

家里的现象很像“某个设备开始乱发包”或者“某条链路出现了奇怪的路由回流”:部分设备能上网,部分设备不能互访,甚至在同一个局域网里,两个本该能直连的设备会突然访问失败。因为家里同时跑着 NAS、软路由、交换机、AP、几套 Docker 服务,所以第一反应通常不会是“Docker 网络池坏了”,而是“是不是某个物理设备又出幺蛾子了”。

这种排查很容易走弯路,因为 Docker 的问题经常不是“挂了”,而是“还能跑,但跑得不对”。它不会像磁盘坏块那样直接报错,也不会像网卡掉线那样一眼能看出异常。它更像是一种慢性污染:新网络越建越多,默认地址池越用越深,直到某一天,宿主机和局域网的地址规划开始打架。

我在 NAS 上看到的,就是这种慢性污染累积到临界点后的结果。

Docker network conflict before and after

图里左边是问题状态:Docker 的自动网络已经挤进了 192.168.x.x,而家里的 LAN 也在 192.168 这一大类里,两边都在同一个大类里,哪怕不是完全相同的子网,也足够让路由和访问关系变得复杂。右边是修正后的状态:Docker 自动网络统一收敛到 172.16.0.0/12,它和家里的 LAN 明确隔离开,宿主机、容器、局域网之间的关系也会干净很多。

Docker 为什么会自己分到 192.168

很多人第一次遇到这个问题时,会下意识以为 Docker 是“随机乱选”了一个网段。其实不是,Docker 的 user-defined network 背后是有地址池策略的。

当你通过 docker compose、Portainer、Dockhand,或者直接 docker network create 创建一个 bridge 网络时,如果没有显式指定 ipam 配置,Docker 会从默认地址池里自动找一个可用的 subnet。这个默认池在很多环境里并不会主动避开你的家庭 LAN,也不会自动感知你当前路由器到底在用什么网段。它只管自己内部的分配规则。

在容器数量少的时候,这个问题几乎感觉不到。因为前几个网络通常还会落在比较“安全”的地址范围里。但当你长期使用 NAS 跑各种服务,尤其是不断增删 stack、反复重建项目网络时,Docker 分配出去的 subnet 会越来越多。这个时候,如果默认池设置得不够保守,或者历史上某次迁移留下了旧配置,就很容易开始出现 192.168.* 这种和家庭网络高度重叠的地址。

对外人来说,这也许只是“一个子网而已”;但对一个家庭网络来说,这其实是在给路由表埋雷。因为家庭网络里常见的设备扫描、局域网发现、访问控制、NAS 互联、反向代理、甚至某些手机 App 的局域网探测,都会受到这种重叠地址的影响。

所以这次我没有继续“见招拆招”,而是直接把规则定死:以后 Docker 的自动网络只能从 172 私有段里分配。

为什么选 172,而不是继续用 192.168 或 10

这个选择不是拍脑袋。

192.168.0.0/16 在家庭网络里非常常见,尤其是一些老路由器、光猫、AP、NAS 初始化向导,都会默认往这个段上靠。只要你住在一个和我类似的环境里,家里有一台主路由、若干 AP、NAS、交换机,再加上一些临时设备,192.168 就很容易被占用得很乱。

10.0.0.0/8 虽然也是 RFC1918 私有地址,但它在很多公司 VPN、云厂商 VPC、虚拟化平台、开发测试网络里都很常见。也就是说,它虽然“理论上安全”,但现实世界里的重叠概率并不低。尤其是你家里还跑软路由、远程办公 VPN、各类实验网段的时候,10.* 和外部环境撞车的概率并不比 192.168 小多少。

相比之下,172.16.0.0/12 的可用空间足够大,覆盖 172.16.0.0172.31.255.255。如果你把这整段专门留给 Docker 和内网隔离用途,通常能做到两件事:

  1. 和家庭 LAN 拉开距离。
  2. 和外部 VPN、公司网络、常见默认网段也拉开距离。

所以这次我把 Docker 的默认网络池统一收敛到 172 段,本质上是做了一次“网络命名空间的重新划界”。

Default pool migration

上面这张图的意思很直接:不是让 Docker 自己继续猜,而是给它一个明确的边界。Docker 以后只在 172.16.0.0/12 里工作,容器网段就不会再跑到家里 LAN 旁边去碰瓷。

先看现状,再改规则

真正动手之前,我先做了两件检查。

第一件是看 Docker 当前到底用的是什么默认地址池:

docker info --format '{{json .DefaultAddressPools}}'

在 NAS 上,修正后的结果如下:

[{"Base":"172.16.0.0/12","Size":20}]

这个结果很重要,因为它说明 Docker 后续自动创建的网络,都会从 172.16.0.0/12 里按 20 位掩码去切分。size=20 的意思并不是“固定一个网段”,而是把一整块大池子切成很多个可重复分配的小网段。对于大量容器、多个 compose 项目、Portainer stack、Dockhand 项目来说,这种切分方式足够实用。

第二件事是扫一遍已经存在的网络,确认到底有哪些网络还在用旧网段。

docker network ls
docker network inspect <network-name>

我最终找到的旧网络里,最典型的是这些:

旧网络 旧网段 新网段
dockhand_central_data_default 192.168.* 172.16.16.0/20
dockhand_data_default 192.168.* 172.16.32.0/20
gopeed_default 192.168.* 172.16.48.0/20
hawser_data_default 192.168.* 172.16.64.0/20
gsmonitor_gs_monitor_net 172.x 172.16.80.0/20

这里有一个关键经验:不要只盯着“看起来像 192.168 的网络”。如果你已经决定把 Docker 网络统一纳入私有范围,那么任何落在 RFC1918 之外的网段,哪怕它不是 192.168,也值得一起清理掉。因为你真正想要的不是“少一个冲突”,而是“让地址规划可控”。

修改 Docker 默认地址池

NAS 上用的是群晖的 Container Manager / Docker 运行环境,所以配置文件路径和普通 Linux 服务器略有差异。但思路是一样的:把 Docker daemon 的默认地址池改掉。

我做的事情本质上只有一段配置:

{
  "default-address-pools": [
    {
      "base": "172.16.0.0/12",
      "size": 20
    }
  ]
}

如果你是在标准 Linux 上,通常会放到 /etc/docker/daemon.json。如果你是在群晖这类系统上,路径可能会在 Container Manager 的内部配置目录里。路径不一定一样,但规则一定要一样:让 Docker 以后只从你预留的私有池里自动分配网络。

改完以后,必须重启 Docker daemon,让新规则生效。这个步骤不能省,因为 Docker 默认地址池不是“热加载就自动替换”的那类配置。

在这个阶段,我还特意保留了旧配置备份。原因很简单:网络改错比容器停一下更麻烦。只要还有回滚能力,任何基础设施操作都值得先做备份。

现有网络怎么迁

这个部分才是最容易出坑的地方。

很多人看到“把默认池改成 172”之后,会以为问题已经结束了。实际上不是。默认池只影响以后新建的网络,而已经存在的老网络并不会自动变脸。也就是说,如果旧网络已经在 192.168.* 里,你不主动迁移,它们还是会继续躺在那里,只是“以后不再新增更多同类问题”而已。

所以现有网络要么删掉重建,要么逐个替换。对于带数据卷的服务,通常我会优先选择“保留数据,只重建网络”的方式。原因很简单:数据是最贵的,网络是可以重新建的。

在 NAS 上,我是按这个思路处理的:

  1. 先确认哪些容器绑定了目标网络。
  2. 记录网络名和 IPAM 配置。
  3. 对应的 stack / compose 项目停止并重建。
  4. 把网络替换到新的 172.16.* 子网。
  5. 启动容器并检查服务可达性。

实际迁移结果如下:

对外界看起来,这只是几个 subnet 的迁移;但对我来说,这一步的意义在于:整个 Docker 生态里,终于没有 192.168.* 这种会和家里 LAN 混淆的地址了。

Migration path from old networks to private range

这张图更像是迁移流程图:先识别旧网段,再停掉相关 stack,然后以新的 172 段重建网络,最后恢复容器。它看起来简单,但实际执行时最怕两类问题:

第一类是“网络删早了”。如果你在容器还在运行时就把 network 删了,服务会直接中断。

第二类是“配置还写死着旧 subnet”。你把运行时网络迁过去了,但 Portainer stack、Dockhand 模板、compose 文件里还写着旧的 ipam.config.subnet,下次重新部署时它还会把你拉回老坑里。

Portainer 和 Dockhand 里要注意什么

这次用户额外提到,后续可能会继续用 Portainer 或 Dockhand 管理容器。这个提醒很关键,因为这意味着网络规则不能只停留在“当前 Docker daemon 上”,还要落实到“未来创建网络的入口”。

这里有个很容易误判的点:

所以,要保证新增容器也满足规则,只改 Docker daemon 还不够,最好再做一次源头检查:

  1. 检查 Portainer 里所有常用 stack 的网络定义。
  2. 检查 Dockhand 的模板或项目配置。
  3. 检查 docker-compose.yml 里是否有 ipam 段。
  4. 检查是否有人手工创建过 bridge network,并在里面写死 subnet。

如果发现有硬编码网段,最好统一改成 172.16.0.0/12 这个大池子里的某个区间,或者干脆删掉显式 subnet,让 Docker 重新接管分配。对于长期维护的家庭 NAS 来说,第二种方式通常更省心。

再补一句:如果你用的是 macvlanipvlan、Swarm overlay 之类的网络模型,它们虽然也受 Docker 管理,但和普通 bridge 网络不是一回事。默认地址池是 bridge 网络最常见的冲突来源,但不是所有 Docker 网络问题都能靠它一个设置解决。

我最后是怎么验收的

迁移完之后,我做了几层确认。

第一层,确认 Docker 的默认地址池已经生效:

docker info --format '{{json .DefaultAddressPools}}'

第二层,确认当前网络里已经没有 192.168.*10.* 这类会跟家庭或办公网络产生碰撞的 Docker 网络残留。

第三层,确认被迁移的容器全部恢复正常,尤其是 gsmonitor 这一组服务,迁移之后还要确保:

都能正常启动并相互访问。

这个阶段其实最能看出“网络迁移”和“服务恢复”之间的差别。很多故障排查文章只写到“网络改好了”,但实际运维里,真正重要的是服务是否还能被外部访问、监控是否正常、告警是否恢复、以及新的容器是不是还会继续落到正确网段。

为什么这次我不再接受“凑合用”

如果问题只是一个实验环境,或者一台临时测试机,那“先能跑就行”是可以接受的。但这台 NAS 不是临时机,它是家庭里真正长期使用的基础设施。这样的机器上,Docker 网络一旦失控,代价并不只是某个容器重启慢一点,而是整个家庭网络可能会开始出现“时好时坏”的问题。

对这种场景,我最后总结出一个很朴素的原则:

  1. 不要把自动分配交给默认值。
  2. 不要把家庭 LAN、Docker 网络、VPN 网段混在同一个大类里。
  3. 不要指望事后排查比事前规划更省时间。

这次我宁愿多花一点时间把 172 段规划清楚,也不愿意以后再遇到“某个容器一扩容,家里网络就开始抽风”的情况。

这次迁移留下的经验

如果以后你也在自己的 NAS、软路由、家庭实验室,或者一台装了大量容器的服务器上遇到类似问题,我建议直接按这个顺序处理:

  1. 先确认宿主机所在的物理 LAN 网段。
  2. 再确认 Docker 默认地址池。
  3. 再扫一遍现有的 bridge network。
  4. 找出所有显式写死 subnet 的 compose / stack。
  5. 把默认池和手工网段统一迁到一个没有冲突风险的私有范围里。

对我来说,这次最终的答案就是 172。它不神秘,也不复杂,但它足够安全、足够宽裕,而且足够适合长期维护。

家里的网络本来就够复杂了,Docker 不应该再给它添一层不必要的随机性。