Docker容器通信疑难解析:服务名与容器名的DNS解析机制

发布时间:2026/9/26 11:55:16
Docker容器通信疑难解析:服务名与容器名的DNS解析机制 1. 困扰很多人的现象明明都是“名字”为什么一个能通一个不能通我见过太多人在 Docker 网络通信上栽跟头了。最常见的一个场景是这样的用docker run启动了两个容器明明加了--name参数比如一个叫app一个叫db结果在app容器里执行ping db却提示Name or service not known。反过来如果用 Docker Compose 启动一套服务在app容器里ping db却是通的但改成ping db_containerCompose 自动生成的容器名又不通了。两边的“名字”看起来差不多为什么表现完全不同这就要说到 Docker 网络通信中一个非常核心、但常常被忽略的底层机制内置 DNS 的解析规则。服务名和容器名虽然都是“名字”但它们的注册方式、解析来源、作用范围有着本质区别。很多人没搞懂这一点所以遇到问题只能靠反复实验去试运气好能通运气不好就抓瞎。这篇文章我会把这两个名字背后的完整逻辑拆开来讲包括 Docker 内置 DNS 的工作机制、服务名和容器名的本质差异、底层解析的完整路径以及我在实际操作中踩过的坑和排查方法。不管你是刚接触 Docker 的新手还是已经写了一段时间 compose 文件的老手把这块搞明白之后以后再遇到容器间通信的问题基本上能直接定位到原因不用再靠猜。2. 先理解 Docker 内置 DNS所有名字解析的核心2.1 容器里的 /etc/resolv.conf 指向哪里在每个容器内部/etc/resolv.conf文件里的nameserver指向的不是我们平时理解的公共 DNS比如 114.114.114.114 或者 8.8.8.8而是一个特殊的地址127.0.0.11。这个地址是 Docker Engine 内置的一个轻量级 DNS 解析器它监听在容器的 loopback 接口上专门处理容器内部的域名解析请求。你可以随便进入一个运行中的容器验证一下docker exec -it my-container cat /etc/resolv.conf输出通常长这样nameserver 127.0.0.11 options ndots:0这是一个非常关键的设计。Docker 并没有直接修改宿主机上的 DNS 配置也没有给容器配置一个固定的 IP 来充当 DNS 服务器。它是在每个接入 Docker 网络的容器内部注入了一个驻留在 127.0.0.11 的嵌入式 DNS 服务由这个服务统一处理容器内所有进程发出的域名查询请求。2.2 内置 DNS 的解析逻辑从容器名到服务名这个内置 DNS 服务在收到查询请求后会按照一套固定的顺序去匹配。这套顺序大致是这样的先查容器名Docker 会把接入同一个自定义网络的所有容器名登记在案如果查询的域名恰好等于某个容器名就返回该容器在当前网络中的 IP 地址。再查网络别名Network Alias如果容器设置了网络别名而这个别名正好匹配查询的域名就返回对应容器的 IP。最后查服务名这里的服务名就是 Docker Compose 在启动服务时自动给容器注册的网络别名。Compose 会把服务名作为网络别名写进容器的配置里因此 DNS 能识别它。这三个层级就是理解容器间域名解析的钥匙。很多文章只告诉你“服务名能通、容器名不能通”但没告诉你这背后的真正原因是服务名本质上就是 Compose 帮你自动注册的网络别名。2.3 默认 bridge 网络与用户自定义网络的差异这里还有一个极其重要的区分默认 bridge 网络和用户自定义网络在 DNS 能力上是完全不同的。如果你直接用docker run启动容器不指定--network那么容器会接入名为bridge的默认网络。在这个网络里Docker 的内置 DNS不会对容器名做自动解析。也就是说在默认 bridge 网络下两个容器之间直接通过容器名访问是行不通的除非你显式地使用--link参数建立连接。这就是文章开头提到的第一种场景的根源。而一旦你创建了一个用户自定义网络无论通过docker network create还是 Docker Compose 自动创建的默认网络Docker 就会开启内置 DNS 的自动解析功能接入这个网络的容器之间就可以直接通过容器名互相访问了。为什么默认 bridge 网络不开启一方面是为了保证零配置容器的兼容性避免域名解析引起意外行为另一方面默认网络是历史遗留的产物而用户自定义网络才是 Docker 推荐的现代用法。理解了这一点你就能解释很多古怪的现象。3. 服务名和容器名两个不同维度的“身份标识”3.1 容器名生命周期管理工具容器名Container Name是通过docker run --name指定的名字或者在 Docker Compose 中通过container_name字段显式声明的名字。它是 Docker 用来管理容器生命周期的一个标识符主要作用是让你在宿主机上方便地通过docker inspect、docker logs、docker stop等命令操作指定容器。容器名有几个明显的特点在宿主机上容器名是全局唯一的至少在同一 Docker 环境中是唯一的你不能同时存在两个同名容器。它和容器的 IP 地址、网络配置没有直接绑定关系只是一个管理层面的标签。Docker 内置 DNS 在自定义网络中也会把容器名注册为可解析的域名但只有在同一自定义网络内的容器之间才能解析。在默认 bridge 网络下容器名不具备 DNS 解析能力。这是最容易混淆的地方——你在宿主机上用容器名操作没问题就觉得容器名应该是全环境可用的但实际上它的 DNS 解析范围是受限于网络归属的。3.2 服务名Compose 编排中的逻辑抽象服务名Service Name是 Docker Compose 在docker-compose.yml中定义的服务标识。比如下面这个典型的配置services: mysql: image: mysql:8.0 container_name: mysql-container app: image: my-app:latest depends_on: - mysql这里的mysql是服务名而mysql-container是容器名。在app容器内部访问数据库时你应该使用mysql这个服务名而不是mysql-container。为什么因为服务名是 Compose 在创建容器时自动给每个容器添加的一个网络别名Network Alias。当你执行docker-compose up时Compose 会在为mysql服务创建容器假设最终容器名是mysql-container时把mysql注册成这个容器在 Docker 网络中的别名。这样一来同网络内的其他服务通过内置 DNS 查询mysql时就能解析到mysql-container的 IP 地址。服务名是逻辑层的抽象它不关心背后的容器叫什么名字、IP 是多少。甚至当同一个服务有多个容器实例通过scale扩容时服务名会对应多个容器的 IPDNS 会在这几个 IP 之间做轮询负载均衡。3.3 两者对比一张表看懂本质区别对比维度容器名Container Name服务名Service Name指定方式docker run --name / compose 中 container_namedocker-compose.yml 中 services 的 key本质身份宿主机上 Docker 管理容器的标签Compose 注册到网络中的别名DNS 注册方式Docker 内置 DNS 直接登记容器名Compose 自动添加 network alias作用范围仅在同用户自定义网络内可解析在同网络内可解析且同一服务多实例时适用轮询是否唯一宿主机全局唯一同一 Compose 项目内唯一但可重复部署是否需要显式配置必须显式指定否则 Docker 自动生成随机名在 compose 文件中天然存在扩容场景每个容器名独立互不相关多个容器共享同一个服务名DNS 可负载均衡3.4 命名方式的不同随机名与项目前缀还有一个容易被忽视的细节容器名不指定时Docker 会随机生成一个名字比如focused_moser、keen_hopper这种名字根本无法在 DNS 中使用。而 Compose 自动生成的容器名则带有项目前缀比如myproject_mysql_1。如果你在另一个容器里ping myproject_mysql_1其实也可以通但前提是你得知道这个完整的容器名——而这恰恰是很多人不愿意深究的地方因为在 compose 体系中你根本没必要去用它。我遇到过一个兄弟写了个配置container_name没写然后记错了自动生成的容器名在另一个容器里用ping myproject_mysql去测试少了个_1折腾了半小时才发现问题。回头把container_name: mysql-container加上用mysql-container就可以解析了但他又问那mysql为什么不行这个时候你已经能给出答案了——因为服务名mysql是 Compose 自动注册的别名它是由服务定义决定的和容器名无关。4. 底层解析的完整路径一个域名从发起请求到返回 IP 的全过程4.1 请求进入容器内部先撞上 127.0.0.11来走一遍完整的解析流程。假设我们在一个 Compose 项目里有app和mysql两个服务在app容器里执行ping mysql。整个过程是这样的首先ping这个程序会调用操作系统的getaddrinfo来解析mysql这个域名。容器的/etc/resolv.conf已经写好了nameserver 127.0.0.11所以系统会把这个解析请求发送到容器内部的 127.0.0.11:53 端口。这个端口上对应的就是 Docker 内置 DNS 服务。内置 DNS 服务收到请求后不是立刻就去外部找 DNS而是先在自己的内存缓存里查找。这个缓存里记录了当前网络内注册过的所有 DNS 记录包括容器名、服务别名、自定义别名等。它先查有没有叫mysql的精确匹配记录如果有就直接返回对应的 IP 地址如果没有再尝试模糊匹配还查不到才把请求转发到容器外部的 DNS 服务器也就是宿主机配置的 DNS。这个内部缓存的查找过程非常快通常只有几毫秒。这也是为什么容器间通过服务名通信性能开销几乎可以忽略不计的原因——整个过程完全走的是本地 loopback 接口根本不经过宿主机网络栈。4.2 Compose 是怎么把服务名注册为别名的关键问题来了Compose 到底是怎么把服务名变成网络别名的我实际操作时用docker inspect看过mysql容器的详情在Networks字段下有这样一段配置Networks: { myproject_default: { IPAddress: 172.20.0.2, Aliases: [ mysql, mysql-container ] } }注意看Aliases数组里有mysql这个就是 Compose 自动添加的服务名别名mysql-container是容器名同时也是别名。每个接入自定义网络的容器Docker 都会自动把容器名注册为别名而 Compose 会额外把服务名注册为别名。所以两者都能被解析只不过前者是 Docker 底层机制后者是 Compose 层面的行为。如果在 compose 文件里为某个服务配置了额外的networks别名这些自定义别名也会出现在Aliases数组里。比如services: mysql: networks: default: aliases: - database - db-server这时同一个容器内部会有mysql、mysql-container、database、db-server四个可解析的名字全部指向同一个 IP。4.3 多个实例时 DNS 如何处理轮询与代表性问题当同一个服务扩容成多个容器时DNS 返回的结果就值得说道说道了。Docker 内置 DNS 会对同一个服务名登记多个 A 记录并在 DNS 查询时按轮询round-robin方式返回其中一个 IP。这意味着在多个副本之间请求会被分散到不同的容器上处理。但这里有一个很多人没注意到的“代表性陷阱”如果你进入某个容器里面用ping mysql来测试连通性你只会看到它解析到了某一个副本的 IP如果这个副本本身有问题你可能会误判整个服务不可用。正确的方式是多次查询或者直接用应用层连接测试而不是单次 ping 就下结论。那容器名呢在同一服务扩容后每个容器都有自己的唯一容器名比如myproject_mysql_1、myproject_mysql_2每个容器名都只对应一个 IP没有任何负载均衡可言。这就是为什么在 Compose 体系里跨服务通信强烈推荐使用服务名——它带来的一定是服务整体的视图而不是单个容器的视图。4.4 External 网络下服务名的可见性问题再扩展一个场景如果你的服务要跟其他 Compose 项目的容器通信就需要用到external网络了。在这种网络里服务的别名会注册到外部网络但只注册 Compose 文件中你显式声明的别名。默认情况下Compose 不会把服务名注册到 external network 上只会注册到项目自己的默认网络上。所以你经常需要这样写services: api: networks: default: aliases: - api-service shared-network: aliases: - api不这么做的话在另一个项目里通过api去访问它会失败。这个坑我在做多项目联动部署时踩过好几次后面会专门展开讲。5. 实操验证自己动手把解析链路看个明白5.1 实验一自定义网络下容器名的解析先来一个基础实验。创建网络并启动两个容器docker network create demo-net docker run -d --name demo-a --network demo-net nginx:alpine docker run -d --name demo-b --network demo-net nginx:alpine进入demo-a容器测试一下对demo-b的解析docker exec -it demo-a /bin/sh / # nslookup demo-b正常输出应该是Server: 127.0.0.11 Address: 127.0.0.11:53 Non-authoritative answer: Name: demo-b Address: 172.18.0.3说明在同一个自定义网络里Docker 内置 DNS 确实能解析容器名。你可以直接用ping demo-b验证网络连通性也是通的。5.2 实验二默认桥接网络下容器名的解析现在换一种方式不加--networkdocker run -d --name default-a nginx:alpine docker run -d --name default-b nginx:alpine docker exec -it default-a nslookup default-b这时候你会得到一个解析失败的错误。原因我在前面说过默认的bridge网络不启用容器名解析。也就是说在同一个默认 bridge 网络里的两个容器虽然 IP 上能互相访问但名字解析是不行的。如果你非要在默认网络里用容器名通信唯一的办法是加--link default-b:db但这种方式已被官方明确标记为遗留功能强烈不建议在新项目中使用了。5.3 实验三Compose 下服务名与容器名的解析再来看 Compose。最简单的配置services: web: image: nginx:alpine container_name: web-container api: image: nginx:alpine container_name: api-container启动后在web-container里分别测试两个名字docker exec -it web-container getent hosts api-container docker exec -it web-container getent hosts api观察一下api-container能解析出 IPapi同样能解析出 IP而且解析到的是同一个地址。这背后的原因就是api在Networks[*].Aliases里被注册为额外的别名了。5.4 用 docker inspect 验证网络别名理解第一手信息如果你想自己验证别名的存在最直接的方法是docker inspectdocker inspect api-container --format {{json .NetworkSettings.Networks}}或者用 jq 格式化docker inspect api-container | jq .[0].NetworkSettings.Networks这里能看到容器接入的网络名、IPAddress、Aliases 列表。你会在 Aliases 里同时看到api-container和api这两个名字。这套信息就是所有 DNS 解析的底层依据。学会看这玩意比在网上搜各种“为什么ping不通”的答案都管用。6. 实战中踩过的坑网络通信问题的排查与避坑指南6.1 同一个 compose 项目里服务名突然解析不到我遇到过最诡异的情况是compose 文件没变昨天ping mysql还通今天重启之后就不通了。查了半天发现是启动顺序的问题——app服务先于mysql服务启动完成在app容器刚开始运行的那几秒里mysql容器的网络别名可能已经注册但由于容器本身还没就绪连接会被拒绝或超时。这不完全是 DNS 解析问题而是服务可用性问题。解决办法很简单在应用代码里增加重试逻辑比depends_on更可靠。depends_on的默认行为只是控制容器启动顺序不保证服务内部进程已经就绪。如果你依赖“启动即可用”这种假设就很容易遇到这种间歇性失败。我建议在应用代码里做连接重试或者用 compose 的 healthcheck depends_on condition 组合来确保依赖就绪services: mysql: image: mysql:8.0 healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 10 app: image: my-app:latest depends_on: mysql: condition: service_healthy6.2 宿主机上访问正常容器里访问不通的三大原因经常有人问我在宿主机上curl localhost:3306是通的但进入容器里curl mysql:3306不通为什么这个问题通常有三个原因第一端口未映射到容器内。localhost:3306能通是因为你做了端口映射容器内访问mysql:3306走的是容器网络这条链路要求mysql服务监听在容器内的 3306 端口并且访问方的网络能路由到目标容器。如果mysql服务只绑定了127.0.0.1比如 MySQL 配置里 bind-address 写成 localhost那容器内其他进程访问就会不通。第二不在同一个 Docker 网络。比如一个容器在 A 网络另一个在 B 网络即使它们在同一台宿主机上内置 DNS 也不认识对方。你需要把两个容器加入同一个自定义网络。第三目标容器没有启动或者处于异常状态。这个最基础但也最容易忽略尤其是在查了半天配置之后才发现容器已经崩了。6.3 不要滥用 container_name与扩展和迁移的冲突很多刚接触 Compose 的人习惯显式指定container_name觉得这样直观、好记。实际上这会引入两个问题第一指定了 container_name 的服务无法扩容。Compose 要求容器名在环境中唯一如果你想docker-compose up --scale mysql3而mysql又显式写了container_name会直接报错。服务名的设计初衷之一就是允许一个服务有多个容器实例显式容器名把这个能力给锁死了。第二同一个 compose 文件在多个环境中部署时容易冲突。比如你本地跑一套CI 上又跑一套如果都指定了相同的container_name而它们又恰好共享同一个 Docker 守护进程就会因为名字冲突导致启动失败。所以我的原则是除非有明确的管理需求否则不要在 compose 文件里写 container_name让 Compose 自动生成带项目前缀的容器名。6.4 network_mode: host 下的解析失效如果你发现服务名突然“完全不可用”了首先要检查服务是不是设置了network_mode: host。在这种模式下容器直接共享宿主机的网络命名空间Docker 不会为它分配独立的网络接口也不会注入/etc/resolv.conf到 127.0.0.11。容器的 DNS 配置直接沿用宿主机的所以它跟其他 Docker 容器之间不会通过内置 DNS 进行名称解析。你用docker exec进去cat /etc/resolv.conf里面根本看不到127.0.0.11。这种模式适合需要高性能网络、或者依赖大量固定端口的场景但代价就是丧失了 Docker 网络的很多能力。如果你需要同时使用 host 网络和容器间通信只能通过 IP 或者宿主机端口去访问没法享受服务名的便利。6.5 跨项目通信时服务名的可见性这个坑前面提过但实操的时候依然会被忽视。假设你有两个 Compose 项目提前创建了一个外部网络shared-net项目 A 里的服务接入了这个外部网络项目 B 也接入了你想在项目 B 里通过项目 A 的服务名访问它。默认情况下这是不行的。因为 Compose 为每个服务添加的网络别名只作用于该项目自己的默认网络。要跨项目通信必须在 compose 文件中把服务名声明到外部网络上# 项目 A services: api: networks: default: shared-net: aliases: - api然后项目 B 里通过api这个服务名就能解析到项目 A 中api容器的 IP。这一步我踩过两次才彻底记住因为平时单个项目跑得好好的根本不会想到跨项目时别名竟然“丢了”。6.6 静态 IP 与固定别名的配合方案最后说一个实际用得上的配置。如果你确实需要容器拥有固定 IP比如有些场景必须把 IP 写死在配置里可以用ipv4_address配合自定义网络来指定services: mysql: image: mysql:8.0 networks: mysql-net: ipv4_address: 172.20.0.10但使用静态 IP 的同时还是建议配合别名使用而不是依赖 IP。因为 IP 一旦写死后续网络规划调整的成本会很高。别名的优势在于即使 IP 变了其他服务依然可以用同一个名字访问它。7. 理解底层逻辑之后你就能自己判断了做了这么多年容器化部署我的最大感受是Docker 网络通信中的绝大多数问题都不是网络本身的问题而是没有搞懂名字解析的规则。一旦你把服务名和容器名各自的底层逻辑梳理清楚——服务名是 Compose 注册的网络别名容器名是 Docker 管理的标识符而它们能否被解析又取决于容器是否接入了用户自定义网络以及该网络是否启用了内置 DNS——你就会发现一个域名能不能通基本一眼就能判断出来。至于排查工具我习惯的顺序是先进容器cat /etc/resolv.conf确认 DNS 指向了 127.0.0.11再用getent hosts 目标名看解析结果解析不到就用docker inspect去看目标容器的网络别名和接入的网络列表。这套方法能解决九成以上的容器间通信问题。如果你也想给读者分享个小技巧那就是在容器内调试网络问题时少用 ping多用 getent hosts。ping 只回答“通不通”getent hosts能告诉你“到底解析到哪里了”——而后者往往才是问题的根源。