docker compose up拉起来一整套服务,应用容器日志里刷dial tcp: lookup db on 127.0.0.11:53: no such host,数据库容器明明在跑,docker ps看状态也正常。手动重启一下应用容器,有时候就好了,有时候还是不行。这个错在Compose环境里出现频率很高,原因却不止一种,先按报错串分清楚再动手。
三种报错不是一回事
第一种是no such host或者Name or service not known。这是纯粹的名字解析问题,容器压根没找到db对应哪个IP,属于DNS层面。
第二种是connection refused。名字已经解析成IP了,对方端口没在听。这种情况往往是数据库还在初始化、或者监听地址配错,跟DNS无关。之前遇到过一次,查了半天DNS,结果是Postgres还在回放数据目录,压根没开端口。
第三种是Temporary failure in name resolution。连127.0.0.11这个解析器本身都没配上,容器里的/etc/resolv.conf内容不对。
三种对应的排查方向完全不同,看错报错串会白绕一大圈。
内嵌DNS只在自定义网络上生效
Docker从1.10.0、1.11.0,那两版起引入了自定义网络和内嵌DNS。容器加入自定义网络后,引擎会在容器里放一个127.0.0.11的解析器,服务名、容器名、网络别名都注册在上面,解析请求走UDP 53转发过去。
关键点在于:默认的bridge网络不给这个能力。用docker run起两个容器不指定--network,它们落在默认bridge上,互相ping名字就是unknown host,但换成IP就通。Compose之所以能直接用服务名,是因为它自动建了一个以项目名加_default结尾的自定义网络,把所有服务都接进去。项目名默认取目录名,也可以用COMPOSE_PROJECT_NAME改。
所以一旦显式写了networks段,就得保证要互访的服务挂在同一张网络上。只给一个服务写networks、另一个不写,两个就不在同一张网里,名字自然解析不到。
四步查到具体原因
第一步看容器里的解析器配置对不对。健康的容器应该长这样,nameserver后面必须是127.0.0.11:
docker exec <容器名> cat /etc/resolv.conf
第二步直接测名字能不能解析。getent走的是系统解析器,比nslookup更贴近应用的真实行为,很多精简镜像里没装nslookup,用getent照样能测:
docker exec <容器名> getent hosts db docker exec <容器名> getent hosts redis
第三步确认两个容器是不是真在同一张网。network inspect的输出里,Containers那一段应该同时列出两个容器,只列一个就说明没接上:
docker network ls
docker network inspect <网络名> --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}
{{end}}'
第四步看容器实际挂了哪张网、是什么模式。NetworkMode显示host的话,内嵌DNS整套都不生效:
docker inspect <容器名> --format '{{.HostConfig.NetworkMode}}'
docker inspect <容器名> --format '{{json .NetworkSettings.Networks}}'
六个常见成因
1、服务不在同一张网络。Compose里只要有一个服务写了networks而另一个没写,就会被分到不同网络。要么都不写,用Compose自动建的_default,要么都写同一张。跨Compose项目互访则要显式声明external网络,两边用同一个name。实测过一次,两个项目目录名不同,各自建了不同的_default网络,服务名互相看不见,改成外部网络就通了。
2、用了network_mode: host或者docker run --network host。host模式下容器直接用宿主机网络栈,不走内嵌DNS,服务名一律解析不了。这种情况要么改回bridge,要么用extra_hosts手写映射。
3、把容器名或者镜像名当主机名用了。Compose里能解析的名字是services下面那个key,不是image名,也不是container_name。yml里写services: db: image: postgres:16,那连接地址里的主机名就是db。给服务设了container_name之后,容器名也会注册进DNS,但版本之间行为有差别,坚持用服务名最稳。
4、手动docker run起的容器没进Compose网络。这种情况不用重建容器,直接连上去,连完立刻生效,不需要重启:
docker network connect <项目名>_default <容器名>
5、宿主机的DNS有systemd-resolved存根。Ubuntu上/etc/resolv.conf默认指向127.0.0.53,宿主机自己能用,容器里够不着这个地址。表现是容器名能解析、外部域名一概失败。修法是在daemon.json里给引擎指定真实的上游DNS,或者在compose里给单个服务配dns字段:
{
"dns": ["223.5.5.5", "114.114.114.114", "8.8.8.8"]
}
6、防火墙把UDP 53拦了。ufw或者iptables规则把容器网桥的DNS流量挡掉,会表现为时通时不通。放行docker0上的53端口,或者用docker exec进容器拿具体地址试一次就能确认。
resolved不等于ready
名字能解析之后,还会撞上另一种失败:容器起来了但服务没就绪。Compose的depends_on默认只保证容器进入running状态,不判断里面的服务能不能用。Postgres初始化数据目录要十几秒,应用容器两三秒就起来了,必然撞上这个空窗期。
正解是给被依赖的服务加healthcheck,然后依赖方写condition: service_healthy。Compose支持的三种condition分别是service_started、service_healthy、service_completed_successfully,第三种适合迁移任务这种跑完就退出的容器:
services:
db:
image: postgres:18
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
app:
build: .
depends_on:
db:
condition: service_healthy
探测命令尽量用镜像自带的工具,pg_isready、mysqladmin ping、redis-cli ping都比临时装curl稳。start_period给冷启动留的宽限期要够长,JVM类应用加载慢,给短了会被过早判成unhealthy。另外健康检查不健康的容器不会被自动重启,Compose里也不会,应用侧该有的重连逻辑还是得有。
Alpine镜像上的一个老毛病
基于Alpine的镜像用musl libc,解析行为和glibc不一样,IPv6的AAAA查询超时之后才回落到IPv4,表现出来是偶发的五秒卡顿。在compose里加dns_opt就能绕过去:
services: app: dns_opt: - single-request-reopen
另外容器访问宿主机上的服务,别写宿主机内网IP,写host.docker.internal配合extra_hosts的host-gateway,这个是Docker Engine 20.10.0、24.0.0、25.0.0,几版都支持的写法,IP变了也不会失效。
改完配置别只up,最好down掉再up做一次冷启动验证,因为已有的网络不会自动重建。docker compose ps里能看到各容器的健康检查状态,docker compose config可以用来确认yml解析出来是不是想要的那个样子。
版本信息:Docker Engine 28.0.0、29.0.0、29.9.2,这三版分别在2025年2月、2025年11月和2026年10月20日发布,其中29.x把containerd镜像存储转成了默认。老于25的客户端在v29上会被拒绝,报client version is too old。数据截至2026年10月。