秋实-Allenyou 的小窝

稻花香里说丰年,听取 WA 声一片

【记录】一种在主机通过 Docker 容器名解析容器 IP 的方法

2025/11/1

众所周知,Docker 内置了一个 DNS 服务器,会通过 iptables 将容器对外的 DNS 请求劫持到 127.0.0.11:53,以此实现同一 network 内的容器之间通过容器名互相访问的功能。也因此,在我的服务器架构中,几乎所有服务都被放进了容器中运行,并通过自定义 network 动态分配 IP。这样,容器之间的网络命名空间不会冲突,在容器之间互相访问时也不需要记忆 IP,只需要使用容器名 + 端口号即可(例如:MySQL:3306 就可以访问到 MySQL 容器)。

然而,除了容器之间互相访问,主机和容器之间的互相访问也是非常常见的。容器访问主机可以使用 host.docker.internal,而主机访问容器则必须使用 IP。这非常不方便,尤其是需要通过 SSH 隧道访问一些没有通过 Nginx 放通对外服务端口的容器时,我不得不先打开 Portainer 面板 / 使用 SSH 连接服务器,查询到对应容器分配的 IP 后再进行连接。

这显然很不优雅。在此之前,我的解决方法是调整 Docker network 的 DHCP 规则,自动分配仅使用 172.18.0.128/25 段,而将前 127 个地址保留下来(网关占用 172.18.0.1),然后手动将一些经常需要访问的容器 IP 静态分配到这个保留范围内。这样,我就可以通过固定的 IP 访问容器,而不需要每次查询了。

然而,这个实现也不怎么优雅。一方面,这个分配关系完全由人工维护,随着分配到固定 IP 的容器数量增加难免会出现分配重复的问题;另一方面,这个保留段的大小也不好把握,保留多了会造成 IP 浪费(虽然有些杞人忧天,如果一台服务器上真的跑了 128+ 个容器那应该加服务器罢),少了则会不够用。同时,IP 相对而言还是不如语义化的容器名容易记忆,使用时也容易忘记对应关系。

因此,我决定探索一种能在主机通过容器名获取容器对应 IP,从而访问对应容器的方案。显然,这需要我们给主机提供一个能解析容器名的 DNS 服务器。

约定

为了与网络内其他主机相区分,我们约定在主机解析 Docker 容器时,一律通过 容器名.docker 解析。

通过 Docker API 自行实现

确定需求之后,我们很快就能自然而然想到一种解决方案:在本地部署一个 DNS 服务器,当查询的域名为 *.docker 时,就调用 Docker 提供的 RESTful API,查询容器列表,找到对应的容器,然后返回其 NetworkSettings/Networks/network名称/IPAMConfig/IPv4Address 作为 A 记录的值即可。

然而,查询之后,我没有发现写好的开源项目。于是我懒了,决定放弃这种解决方案。

通过 CoreDNS 进行 DNS 请求重写 + 转发

实际上,我们已经有了一种通过 DNS 查询容器名的实现了:Docker 的内置 DNS 127.0.0.11:53。然而,这个 DNS 服务器仅在容器内可用,并且不支持添加自定义后缀进行分流。

因此,我们的需求便转换为了以下几点:

  1. 运行一个 DNS 服务器,监听主机的 DNS 请求。
  2. 对于非 .docker 结尾的查询,直接转发给常规 DNS 或返回失败(利用系统 DNS 的主备机制,排序靠前的 DNS 查询失败时会继续查询靠后的)。
  3. 对于 容器名.docker 格式的查询,将查询请求重写为 容器名 形式。
  4. 容器名 形式查询请求转发给 Docker 内置 DNS 127.0.0.11:53,并返回结果。

既然请求要转发给 Docker 内置的 DNS,那么这个 DNS 服务器显然必须要运行在容器里。考虑到这个功能并不复杂,我们需要一个轻量、支持按规则转发/重写的 DNS 服务器。

这里我选择了 CoreDNS,一个基于 Golang 的轻量 DNS 服务器。它是基于配置文件的,且拥有丰富的内置插件,其中 forward 插件提供了 DNS 转发功能,rewrite 提供了请求重写功能,而且也足够轻量。

首先,我们阅读官方文档后,写出如下配置文件:

docker.:53 {
    log
    rewrite name regex (.*)\.docker {1}
    forward . 127.0.0.11:53
}

首先,我们通过 docker.:53 {} 块定义了一个在 53 端口监听,且只处理 docker. 一级域名下解析的 DNS 服务器。随后,通过 log 插件开启日志,通过 rewrite name 语句调用 rewrite 模块,将查询目标满足 (.*)\.docker 正则表达式的请求重写,去掉后面的 .docker 部分,保留前面的主机名。最后,通过 forward . 127.0.0.11:53 将重写后的请求全部转发给 Docker 内置 DNS 处理。

随后,我们部署一个运行在容器中的 CoreDNS 服务,将其加入我们的 network,为其手动指定一个 IP,并让其加载这个配置文件。

假设容器 IP 为 172.18.0.11,我们只要将 172.18.0.11:53 加入主机系统的备选 DNS 即可。

由于我不使用 NetworkManager 和 systemd-resolved,因此只要直接在 /etc/resolv.conf 中加入一行 nameserver 172.18.0.11:53 即可。

保存之后尝试 ping 一个 容器名.docker 的域名,发现能正确 ping 通。

附:关于域名的解释

上文 docker. 形式的域名在日常生活中使用频率不算特别高,因此在此解释一下:

一般来说,完整的域名末尾以 . 开始,一个 . 表示的是顶级域名,处理 . 域名解析的权威 DNS 服务器也就是著名的 13 台根服务器。

随后的 com. / net. 等为一级域名,但是命名上也称为顶级域名 Top Level Domain(TLD),由对应的注册商进行管理解析。

在此基础上,我们注册的 allenyou.wang. 这种则是二级域名,以此类推还有三级四级域名。

总之,在权威 DNS 中,真正的域名一定是以 . 结尾的。

附:一个可用的 Dockerfile

FROM coredns/coredns:latest

RUN cat << EOF > /Corefile \
docker.:53 { \
    log \
    rewrite name regex (.*)\.docker {1} \
    forward . 127.0.0.11:53 \
} \
EOF

ENTRYPOINT ["/coredns"]

CMD ["-c", "/Corefile"]

【记录】一种在主机通过 Docker 容器名解析容器 IP 的方法

https://www.allenyou.wang/post/30

本文作者

秋实-Allenyou

授权协议

CC BY-NC-SA 4.0

加载评论中……