先说一个多数人踩过的坑:把容器跑起来之后,在另一个容器里直接用容器名去ping,结果发现经常不通;但用 IP 地址就能通。一旦换成docker compose编排,服务名却又能通了。很多人第一反应是“网络模式不同”,这没错,但真正决定两种名字能否被解析的,是 Docker 内建 DNS 这套机制。搞懂“服务名”和“容器名”在底层是如何进入 DNS 解析链路的,网络排障就算入门了。
这篇文章我把 Docker 网络通信中两种名字的底层逻辑拆开聊一遍:Docker 内建 DNS 是怎么工作的、自定义网络和默认 bridge 有什么区别、Compose 里的服务名最终映射到什么、以及排查时最容易忽略的几个细节。
1. 名字解析的本质:DNS 解析链路是怎么搭起来的
在容器的世界里,所谓“用名字通信”,本质上就是一次 DNS 解析。Docker 不会像传统交换机那样维护一张 MAC 地址表,它维护的是一个 DNS 记录表,然后把这个 DNS 服务告诉每个容器。
Docker 创建容器时,会往容器里写入一份/etc/resolv.conf,里面的 nameserver 指向 127.0.0.11。这个地址就是 Docker 内建 DNS 的虚拟 IP,监听在容器的 loopback 上。无论容器跑在哪种网络模式下,只要不是 host 网络,这个 127.0.0.11 就会存在。
Docker 之所以选 127.0.0.11 而不是类似 172.17.0.1 这种网关地址,是因为 DNS 查询必须走 loopback 才不会被本机的路由规则干扰。它通过 iptables 规则把发往 127.0.0.11:53 的 UDP/TCP 流量重定向到 Docker daemon 进程内部的 DNS 服务,这个服务再去查它自己维护的记录表。
这套机制有几个明显特点:
- DNS 解析发生在容器内部,不经过物理网络,所以延迟极低;
- Docker daemon 对容器的名字记录有全局视图,不会出现 DNS 缓存失效问题;
- 每个容器拿到的 DNS 记录只包含它所在网络内的容器,天然做了网络隔离。
理解这一点之后,再去看“容器名 ping 不通 / 服务名通”这类问题,其实就是在问:目标名字有没有被写进当前容器的 DNS 记录表里。
1.1 docker0 默认网络与用户自定义网络的本质差异
默认创建的容器如果没指定--network,会挂在名为bridge的默认网络上,对应宿主机上的docker0网桥。这个网络上,容器可以用 IP 互访,但用容器名互访默认是不通的。
原因不在网桥本身——默认 bridge 上的容器也接入了内建 DNS,名字解析本身是可以工作的。真正的问题在于 Docker 设计上的保守策略:为了让默认 bridge 的行为足够简单,避免新手把容器名当全局域名用,Docker 默认关闭了默认网络上的名字解析功能。
自定义网络就不是这个策略。只要你通过docker network create创建了一个网络,然后把容器都挂上去,内建 DNS 会自动开启,容器名之间立刻可以互相解析。
这里有一个容易被忽略的细节:--link参数在默认 bridge 上也能实现名字解析,但它不是通过 DNS,而是往目标容器的/etc/hosts里写条目,并且只对声明了 link 的容器生效。它跟自定义网络的 DNS 解析有本质区别——hosts 文件是静态的,容器 IP 变了就得重建,而 DNS 记录是动态跟随容器状态的。
1.2 为什么 Docker 推荐用户自定义网络而不是默认 bridge
从我实际使用的体验看,Docker 官方文档里反复强调“使用自定义网络”,不是为了让你多敲一条命令,而是因为默认 bridge 有几个明显不适合生产的地方:
- 缺少内建 DNS 解析:容器多了之后,依赖 IP 互访基本不可维护;
- 没有自动隔离:所有容器都在一个大的二层网段里,没法做细粒度网络策略;
- 连通的容器无法动态解除:默认网络上的容器只要在一个网段就能互访,想临时断开某个容器只能靠防火墙或者停容器;
--link机制是单向且静态的:A link 了 B,A 能通过名字访问 B,B 却不能通过名字访问 A,这种不对称在实际维护时特别容易让人困惑。
自定义网络则解决了这些问题:名字自动解析、网络间隔离、容器可以在网络间自由加入或退出而不用重建。
2. 一次“服务名”解析请求的完整链路
拿一个典型的 Compose 场景拆解:假设有两个服务web和db,在同一个 Compose 项目里。现在web容器里执行ping db,你看不到的是这样一条链路:
web容器发起getaddrinfo("db")调用;- glibc 读取
/etc/resolv.conf,发现 nameserver 是 127.0.0.11,把 DNS 查询包发往 loopback 的 53 端口; - iptables 规则截获这个包,转发给 Docker daemon 内嵌的 DNS 服务;
- Docker DNS 在自己维护的记录表中查找
db这个名字; - 找到对应的容器 IP 后,返回 A 记录;
web拿到 IP,发起 ICMP 或 TCP 连接。
这中间有一个很关键的环节:/etc/resolv.conf除了 nameserver,还包含 search 域。Docker 在写入这个文件时,会根据容器所在的网络自动追加搜索域,格式通常是<项目名>_<网络名>。
2.1 DNS 搜索域与 ndots 的隐藏影响
搜过 Docker 网络问题的朋友可能见过这样的输出:
search default.svc.cluster.local svc.cluster.local cluster.local这个一般是 Kubernetes 环境。原生 Docker 里,搜索域长这样:
search <项目名>_<网络名>比如 Compose 项目叫myapp,网络叫myapp_default,那 search 域就是myapp_default。这个搜索域不是摆设:当你在容器里访问db时,glibc 会按照ndots的规则决定先查db还是先查db.myapp_default。
默认情况下ndots的值是 1,也就是说域名里只要带一个点,就先把整个域名当作绝对域名去查,查不到再追加 search 域。db这个名字本身没有点,所以 glibc 会先在/etc/hosts里找,找不到就用原名字db查一次,失败后再尝试db.myapp_default。
这里容易混淆的点是:名字里带点不代表“一定是个域名”。比如 Compose 服务名如果带了项目前缀,像myapp_db_1,它虽然不含点,但它是 Docker 生成的容器名而不是服务名。服务名永远是不带下标的短名字。
2.2 实测:tcpdump 抓 DNS 查询验证搜索过程
为了验证上面这条链路,我在一个 Compose 环境里做过一次抓包实验。在web容器里执行:
tcpdump -i eth0 port 53 -n然后另开终端执行ping db。抓到的流量非常清晰:先是db的 A 记录查询,紧接着是db.myapp_default的查询。Docker 内建 DNS 对前一个查询返回了记录,所以第二个查询其实多余,但 glibc 并不知情,它只会按顺序尝试。
这个现象引出一个实用结论:服务名解析慢的问题,绝大多数不是 Docker DNS 慢,而是 glibc 的搜索域探测机制多跑了几次无效查询。如果容器里装了nss-myhostname或者修改了ndots配置,行为又会不一样。
2.3 Docker DNS 的响应顺序:服务名优先于容器名
Docker 内建 DNS 维护的记录表里,同一时刻可能有两个名字指向同一个容器:服务名和容器名。比如 Compose 里服务叫db,生成的容器名叫myapp-db-1,这两个名字都能解析到这个容器。
一旦发生名字冲突——比如某个容器名恰好和另一个服务名相同——Docker DNS 的响应顺序是:服务名优先于容器名。这是我在一个实际事故里确认过的:当时有个 Compose 项目,服务名叫redis,另外手动跑了一个容器名也是redis的容器。所有依赖redis这个名字的解析全部指向了 Compose 服务,而手动起的那个只能靠容器全名访问。
这种优先级设计是有意为之:服务名在 Compose 项目内部是稳定的拓扑标识,而容器名只是运行实例的标识。业务代码应该依赖服务名而不是容器名。
3. Compose 里的服务名是如何映射到容器的
Compose 是 Docker 网络通信里最常用到的编排工具,绕不开。很多人在 Compose 里配了links或者depends_on,但没搞懂这些配置到底影响了什么。逐个拆开。
3.1 服务名、容器名和网络别名的三角关系
Compose 创建服务时,默认生成的容器名格式是<项目名>-<服务名>-<序号>,比如myapp-web-1。这个容器名可以被 Docker DNS 解析。
但 Compose 同时会为该容器创建一个网络别名(network alias),别名就是服务名本身。也就是说,在 Compose 自动创建的网络里,web这个名字并不是容器名,而是别名。
网络别名这个概念很关键。你可以手动给容器加别名:
docker network connect mynet mycontainer --alias my-alias加了之后,同一个网络里的其他容器就可以用my-alias来访问mycontainer了。Compose 做的就是把服务名注册为容器的别名,然后把容器加入到项目网络里。
所以 Compose 环境下,服务名解析路径是:
服务名 -> 网络别名 -> 容器 IP
而手动docker run环境下的路径是:
容器名 -> Docker DNS 记录 -> 容器 IP
两条路径最终都落到同一种解析机制上,这也解释了为什么 Compose 服务名和容器名都能被解析,但来源不同。
3.2depends_on和links在网络解析里扮演什么角色
depends_on的作用经常被误解。不少初学者以为它决定了网络连通性,实际它只控制服务启动顺序,不影响名字解析。即使不写depends_on,只要两个服务在同一个网络里,名字解析就能正常工作。
links则有点历史包袱的味道。在 Compose 文件里写:
services: web: links: - db它的效果等效于在启动web容器时加了--link db,本质是把db的容器名、服务名、IP 追加到web容器的/etc/hosts里。
但现在的 Compose 环境里,只要你让服务加入同一个自定义网络,links完全没必要写。它还引入了一个副作用:links会创建一种隐式的依赖关系,导致 Compose 启动时额外等待目标容器完成启动,但这个等待不检查目标服务的可用性,所以根本解决不了“web 起来了但 db 还没就绪”这类问题。
正确的做法是用depends_on加condition: service_healthy配合健康检查,而不是靠links做网络接入。
3.3 服务名解析与健康检查状态的关系
这里有个值得注意的细节:Docker DNS 不会因为容器不健康就把它的记录从表里摘掉。只要容器处于运行状态,它的名字就能被解析。
我遇到过的场景是:Compose 里web依赖db,db的健康检查还没通过,但web已经开始尝试连接db。由于 DNS 能解析出db的 IP,连接请求已经发出,但db端口可能还没监听,于是web疯狂报错。这不是 DNS 的问题,但排障时容易误判成一连串解析失败。
解决方式是在 Compose 里配置:
services: web: depends_on: db: condition: service_healthy db: healthcheck: test: ["CMD", "pg_isready", "-U", "postgres"] interval: 5s timeout: 3s retries: 10让依赖关系真正做到“等待就绪”,而不是只等进程启起来。
4. 核心机制细节:嵌入式 DNS 的解析行为与排障方法
服务名和容器名的底层逻辑讲完之后,来几个实际排障中非常有用但容易被忽略的机制细节。
4.1 为什么 ping 不通但端口能通
和名字解析无关但经常混在一起的现象:容器 A 能解析出容器 B 的 IP,但ping B不通,而用nc探测 B 的端口却通了。
原因通常不是 DNS,而是容器 B 的镜像里没有装ping依赖的 ICMP 工具,或者容器 B 的防火墙规则丢弃了 ICMP。DNS 解析只是把名字转成 IP,能不能连通取决于目标容器是否响应这个协议。
排障时要记住:先区分“解析不到”和“到了但没响应”。前者看 DNS 返回,后者看具体协议和端口,这两件事的排查思路完全不一样。我用一句话总结:服务名和容器名的通信链路里,DNS 只是导流,流量能不能到达应用层,是网络策略和进程监听的问题。
4.2 容器名到 IP 的映射在跨网络时为什么会失效
每个自定义网络是一个独立的 DNS 视图。同一个容器如果同时连接了网络 A 和网络 B,它在网络 A 里的名字解析记录和在网络 B 里的记录是互相独立的。
如果容器 X 只连接了网络 A,它去解析网络 B 里的容器名,得到的不是“解析失败”,而是NXDOMAIN,即无法找到对应记录。这个“解析失败”本身是符合预期的,因为 Docker 不会跨网络维护名字视图。
遇到这种跨网络通信需求,常规方案是让目标容器也接入当前网络:
docker network connect net-b container-b或者直接在 Compose 里声明两个网络:
services: app: networks: - net-a - net-b但要注意,跨网络连接后,容器会同时拿到多个网络的 IP,这时目标容器需要监听在所有接口上(比如0.0.0.0),否则来自另一个网络的流量虽然能到达,但应用只监听在特定 IP 上,依然连不上。
4.3 hosts 文件与嵌入式 DNS 的优先级
容器里的/etc/hosts不是摆设。glibc 的解析顺序是:先查/etc/hosts,再查 DNS。Docker 创建容器时会自动往 hosts 文件里写入容器的自身记录和--link指定的条目。
如果你手动用docker exec改了容器的/etc/hosts,这个改动会在容器重启后丢失,因为 Docker 每次启动容器会根据当前网络配置重新生成这个文件。正确做法是用extra_hosts配置:
docker run --add-host mydb:192.168.1.10 ...Compose 里对应:
extra_hosts: - "mydb:192.168.1.10"这种方式的优先级高于 DNS 记录,适合临时指定外部地址或多个环境切换的场景。
4.4 高频排查场景:一条命令定位容器名解析问题
排查名字解析问题时,我的固定套路是四步:
第一步,确认容器所在网络:
docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' <container>第二步,确认目标容器在该网络的 IP:
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <target>第三步,进入容器测试 DNS 解析:
docker exec -it <source> getent hosts <target-name>如果返回了 IP,说明 DNS 没问题;如果没返回,说明目标名字不在当前网络的解析视图里。
第四步,查看容器内的/etc/resolv.conf:
docker exec -it <source> cat /etc/resolv.conf确认 nameserver 是否为 127.0.0.11。如果容器是--network host模式,这个文件会是宿主机的配置,名字解析自然不生效。
这套流程基本能覆盖 90% 的容器名通信问题,不用装额外工具,纯靠 Docker 原生命令就能完成。
5. 实际问题排查与避坑经验
整理几个我在实际项目里遇到过、并且真实浪费过时间的问题。
5.1 服务名带了项目前缀,导致解析到错误的容器
Compose 项目里,默认服务名解析没问题,但如果你用container_name显式指定了容器名,并且这个名字和其他服务重名,就会出现解析歧义。
看一个典型配置:
services: db: container_name: mydb此时网络别名仍然是db,但额外多了一个容器名mydb。如果另一个服务里写的是连接mydb,也能通,因为容器名同样会被注册到 DNS 里。但这种写法的问题在于,一旦你横向扩缩容,比如docker compose up --scale db=3,container_name就不能用了,因为名字冲突。更推荐的做法是不要设置container_name,让 Compose 自动生成带序号的容器名,业务代码里固定用服务名。
5.2 容器重启后 IP 变化,但名字还能解析
这是自定义网络和内建 DNS 的典型优势:容器重启后 IP 会重新分配,但其他容器通过服务名访问不会受任何影响,因为 DNS 记录是实时跟随容器状态的。
但这个机制有一个前提:使用自定义网络。如果在默认 bridge 上用了--link,容器重启后 hosts 文件里的旧 IP 不会自动更新,这就是为什么我一直强调生产环境一定要自定义网络。
5.3 daemon 重启后,跨主机网络的解析异常
单机 Docker 环境里,daemon 重启后 Docker 会重新创建网桥和网络,容器的 IP 会变化,但服务名解析还是能恢复。如果用的是 Swarm 或者跨主机 overlay 网络,情况就不一样了。
Overlay 网络里,服务名解析依赖 Docker 内建 DNS 在集群节点间的同步。某个节点上的 daemon 重启后,DNS 记录可能需要几秒钟才能在全集群重新同步,这个窗口期内容器解析服务名有可能短暂失败。处理方式是在应用层做重试,或者把依赖方的启动延时调高一点。
5.4 网络策略和 DNS 解析的边界
最后提醒一个容易让人误判的边界:Docker 的嵌入式 DNS 只解决名字到 IP 的映射,不负责网络连通性。即使名字解析成功,容器 A 到容器 B 的流量还可能被以下因素拦截:
- 宿主机 iptables 规则;
- Docker 网络间的隔离策略(比如两个容器分别在不同自定义网络);
- 容器内应用监听的 IP 和端口。
遇到名字能解析但连接失败时,不要一味在 DNS 上下工夫,先确认流量真正到了哪里。用tcpdump在目标容器上抓包是最直接的验证方式,我在排障时几乎每次都靠这一招快速定位问题到底在网络层还是应用层。
6. 经验复盘与实用建议
关于服务名和容器名的底层逻辑,我再强调几个实操中最容易踩坑的点,这些是文档里不会细写的内容。
6.1 可视化管理网络拓扑,减少“想当然”的排障
容器一多,网络的拓扑关系就不靠脑子记了。我一般用docker network inspect配合jq输出每个网络下挂的容器和 IP:
docker network inspect mynet | jq '.[0].Containers | to_entries[] | {name: .value.Name, ipv4: .value.IPv4Address}'这样能一眼看出来当前网络的完整成员列表,排查“为什么解析不到”时就清楚了:你要访问的目标容器到底在不在同一个网络里。
6.2 服务名解析不是“全局可用”的
不少人会习惯性地以为 Docker 和 Kubernetes 一样,服务名是全集群可解析的。这个理解是错误的。Docker 的服务名解析是“网络内可见”的,不在同一个自定义网络里的容器,即使部署在同一台宿主机上,也解析不到对方。
这也是容器集群迁移到 Kubernetes 之后最容易踩的坑:K8s 里 Service 是集群级别的 DNS 名称,天然全集群可解析。如果带着 Docker 的经验去配置 K8s,很容易忽略网络策略层面的隔离,导致服务被不该访问的客户端扫到。
6.3 给容器和服务起名时的规范性建议
名字不是随便起的,在实际维护中名字的规范性直接决定了排障效率。我的建议是:
- 容器名不要包含下划线,DNS 和某些工具对下划线的支持不统一;
- 服务名使用短横线连接的小写单词,比如
user-service,不要用驼峰; - 服务名和容器名尽量不手动设置别名,避免重复映射造成困惑;
- 如果业务代码需要访问数据库,统一用服务名配置连接串,不要写容器名或 IP。
这套规范在 Docker 和 Compose 之间切换时基本可以无缝衔接,也方便以后迁移到编排平台。
回到最开始的问题:容器名和服务名在 Docker 网络通信中为什么都能用?答案其实很简单——它们都被注册进了 Docker 的内建 DNS,只是来源不同。容器名是 Docker daemon 自动生成的记录,服务名是 Compose 通过网络别名注册的记录。理解了这层映射关系,再遇到名字解析失败,你就能快速判断问题出在哪个环节,而不是靠重启容器碰运气。