bigywater.tech
Docker 系列 · 第四篇

两个容器在默认网络下,为什么用名字互相连不上,换个网络就通了?

接着系列前几篇的 FROM scratch 套路,这次写一对真实的 TCP 服务端/客户端小程序,在默认 bridge 网络和自己建的 bridge 网络下分别测试容器间怎么通信——本机真实复现了一个几乎所有人都踩过的坑:同样的代码,换个网络,行为完全不同,而且原因直接写在容器自己的 /etc/resolv.conf 里,不用猜。

no such host
本机真实实测:默认 bridge 网络下,用容器名连接真实失败——只能用 IP
127.0.0.11
本机真实实测:自定义网络下 /etc/resolv.conf 真实多出 Docker 内嵌 DNS,容器名解析立刻能用

真实实测:默认 bridge 网络,只能用 IP,不能用容器名

写一对真实的 TCP 程序——服务端监听 :9000 收到一行就回一句话,客户端连上去发一行、打印回复。

真实实测
$ docker run -d --name srv1 netserver:scratch
$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' srv1
172.17.0.2
真实实测
$ docker run --rm netclient:scratch 172.17.0.2   # 用 IP,成功
$ docker run --rm netclient:scratch srv1         # 用容器名,失败
got reply: hello from 82c692f88326, you said: hi from client dial error: dial tcp: lookup srv1 on 192.168.65.7:53: no such host

同一个 srv1,用真实分配到的 IP 172.17.0.2 连接完全没问题,换成容器名就直接解析失败——这不是这套代码写错了,是默认 bridge 网络本身的设计:它不提供容器名到 IP 的自动解析。

真实实测:换成自定义网络,容器名立刻能用

同样的镜像、同样的代码,唯一的变量是网络。

真实实测
$ docker network create mynet
$ docker network inspect mynet --format '{{json .IPAM.Config}}'
[{"Subnet":"172.18.0.0/16","Gateway":"172.18.0.1"}]
真实实测
$ docker run -d --name srv2 --network mynet netserver:scratch
$ docker run --rm --network mynet netclient:scratch srv2   # 直接用容器名
got reply: hello from c52e2400efe5, you said: hi from client

自定义网络先拿到了一个完全独立的子网(172.18.0.0/16,跟默认 bridge 的 172.17.0.0/16 是两个不相交的段),而且这次直接用容器名 srv2 就真实连通了——同一套代码,换个网络就从"必须硬编码 IP"变成"可以用名字互相找到对方"。

真实实测:原因写在 /etc/resolv.conf 里,不用猜

直接对比两种网络下容器自己看到的 /etc/resolv.conf。

真实实测
$ docker run --rm resolvcheck:scratch                    # 默认 bridge
nameserver 192.168.65.7 # Based on host file: '/etc/resolv.conf' (legacy)
真实实测
$ docker network create mynet2
$ docker run --rm --network mynet2 resolvcheck:scratch   # 自定义网络
nameserver 127.0.0.11 options ndots:0 # Based on host file: '/etc/resolv.conf' (internal resolver) # ExtServers: [host(192.168.65.7)]

默认 bridge 网络下,容器的 DNS 直接指向宿主机的真实 DNS(192.168.65.7),查询 srv1 这种名字自然只会当成一个不存在的域名去查外部 DNS,查不到;自定义网络下,nameserver 变成了 127.0.0.11——Docker 在每个自定义网络里都跑了一个内嵌 DNS 服务器,认识这个网络里所有容器的名字,查不到的名字才会转发给真实的外部 DNS(ExtServers 那一行就是转发目标)。这也是 Docker 官方文档里明确写的行为:

Docker 官方文档 · Networking overview · docs.docker.com/engine/network With the default configuration, containers attached to the default bridge network have unrestricted network access to each other using container IP addresses. They cannot refer to each other by name. Once connected to a user-defined network, containers can communicate with each other using container IP addresses or container names.

真实实测:-p 端口映射,从宿主机直接连进容器

前面几组实验都是容器之间互相访问,这次反过来——从运行 docker 命令的这台 Mac,直接连容器内部的服务。

真实实测
$ docker run -d --name srv3 -p 19000:9000 netserver:scratch
$ printf 'hi from host\n' | nc 127.0.0.1 19000
$ docker port srv3
hello from 10d1a7fe75cc, you said: hi from host 9000/tcp -> 0.0.0.0:19000 9000/tcp -> [::]:19000

容器内部这个进程自己完全不知道"19000"这个端口的存在——它只认识自己监听的 :9000。-p 19000:9000 在宿主机这一侧开了个监听,把发到 19000 的流量转发进容器网络命名空间里的 9000 端口,对容器里的程序完全透明。宿主机这边发起的 nc 请求,真实穿过这层转发拿到了容器里程序的真实回复。

演示:同一次连接尝试,两种网络下的不同结局

对应上面的真实实验,切换"默认 bridge"和"自定义网络"两种场景,看同一次"用容器名连接"走到哪一步。

容器名解析 · 两种网络对照
默认 bridge
步骤结果

数据直接取自上面真实实测的 resolv.conf 差异和真实的连接结果(默认 bridge 下 no such host,自定义网络下真实拿到回复),演示动画只是把"查 DNS → 有没有内嵌解析器 → 连不连得上"这条链路逐步呈现。

参考与说明

  • 本文全部真实实验(默认 bridge 下容器名解析真实失败、自定义网络下容器名解析真实成功、两种网络各自的子网段、两种网络下真实不同的 /etc/resolv.conf 内容、-p 端口映射从宿主机真实连进容器内部服务)均在本机真实运行的 Docker Desktop 上完成,用的是跟前几篇一样的 FROM scratch + 本机交叉编译静态二进制技巧(一对真实的 TCP 服务端/客户端程序),全程零网络镜像拉取,数据未做删改。
  • 行为引用(默认 bridge 与用户自定义网络在容器名解析上的差异)取自 Docker 官方文档 docs.docker.com/engine/network——这段文档描述的行为已经用上面的真实实验逐条验证过,不是照抄文档就直接采信。
  • 演示动画的两种场景数据直接取自真实实测的连接结果和真实的 resolv.conf 内容,没有额外模拟或编造的步骤。
  • 没有涉及:Docker 内嵌 DNS 服务器(127.0.0.11)的具体实现细节(在 moby/moby 的 libnetwork 里)、overlay 网络驱动(跨宿主机的容器网络,需要多台真实机器,这台机器没法验证)、--link 这个已废弃的旧式容器互联方式、真实的 iptables NAT 规则细节(Docker Desktop 的 Linux VM 屏蔽了对这层的直接观察)。
☕ 如果这篇文章帮到你,可以请作者喝杯咖啡 · 爱发电
京ICP备2026067502号-1京公网安备11010502063487号