VPN 已经连上,浏览器能访问内部站点,容器却仍然解析不了同样的域名。安装软件包时因 EAI_AGAIN 这类名称解析错误而中断,或者应用找不到一个宿主机能正常解析的数据库主机。容器根本没有使用 VPN 安装的解析器。Docker 在创建容器时复制了宿主机的解析器配置,此后没有任何东西再复制一次。

同一台机器上的两种解析器视图

在 Linux 上,Docker 在创建容器时会把 宿主机的解析器配置复制给每个容器,过滤掉环回地址,并在 127.0.0.11 上运行一个内置 DNS 服务器向外转发查询。容器启动之后,VPN 客户端再改写 /etc/resolv.conf 也不会影响容器内部。重启容器只有在那一刻宿主机的解析器恰好正确时才有用。

macOS 或 Windows 上的 Docker Desktop 又多了一层,因为守护进程运行在虚拟机里。宿主机的 VPN 解析器可能永远对那个虚拟机不可见,所以重启完全不起作用。在选择方案之前先比较这两种视图:把 docker exec mycontainer cat /etc/resolv.conf 与宿主机的解析器列表放在一起看。

宿主机列出内部解析器,容器却列出公共解析器时,容器能解析公网域名,却会在内部域名上失败。这种分裂正是该问题的特征。

方案 A:按容器传入隧道解析器

Docker 支持按容器设置 DNS,这是能起作用的最小改动。先读取 VPN 的解析器地址:macOS 上用 scutil --dns,Linux 上用 resolvectl status 或客户端自己的状态输出。然后用这些地址启动容器,例如 docker run --dns 192.168.2.1 --dns 192.168.123.1 myimage。在 Compose 里,同样的设置是服务下的一个列表,它留在版本控制里,而不是某人的 shell 历史里。

/etc/docker/daemon.json 中的 dns 数组对每个新容器生效,听起来更整齐,其实不然。隧道断开时它也照样生效,此时只有通过 VPN 才能访问的解析器会破坏普通的镜像拉取。按容器设置更容易讲清楚,也更容易撤销。

方案 B:让容器共享宿主机网络

宿主机网络完全绕过了这次复制,因为容器直接使用宿主机的解析器和宿主机的隧道。但它也去掉了你大概想要的网络隔离,而且在 Docker Desktop 下行为并不相同。在 Linux 上,对于必须解析内部域名的一次性工具来说,这是务实的选择。

对构建来说两种方案都不适用,因为构建步骤不会继承运行时参数。在平台允许的地方用宿主机网络构建,或者把依赖安装放在不需要隧道的阶段。

两种方案都解决不了的问题

如果容器没有到该地址的路由,DNS 地址就毫无用处。VPN 的解析器通常位于隧道的地址空间内,所以容器需要一条通往该前缀的路径。Docker 的默认网桥一般通过宿主机向外路由,当宿主机有隧道路由且开启了转发时就足够了。自带地址池的自定义网桥网络可能与 VPN 子网冲突,那时查询就会去往完全不同的地方。

在排查解析器本身之前,先核对 Docker 网络的子网与 VPN 子网。地址重叠会产生一个看起来和坏掉的 DNS 服务器一模一样的故障。

在容器内部验证

在容器内部用 getent hosts db.internal 测试,而不要用浏览器或语言运行时,因为它们会套用自己的 DNS 策略和自己的缓存。如果 getent 能解析该名称而应用仍然失败,说明解析器已经正确,剩下的问题在应用层。如果它解析不了,DNS 这一步就没有完成。

改完 DNS 设置后要重新创建容器。在运行中的容器里编辑 /etc/resolv.conf 会随容器一起消失,并把这个配置问题一直掩盖到有人重启它为止。

相关文章