Tech Digest

Guide

Why can't my Docker container resolve DNS?

Docker never hands a container your host's DNS setup unchanged. It rewrites `/etc/resolv.conf` per network, and the rewrite treats a resolver on 127.0.0.1 or 127.0.0.53 differently on each network type. The file inside the container says which rewrite it got, and that usually names the fault.

Last reviewed

Why can't my Docker container resolve DNS?

Usually because the container was given a nameserver on a loopback address, such as 127.0.0.1 or 127.0.0.53, which inside the container points at the container itself. Run docker exec <name> cat /etc/resolv.conf. If it lists 127.0.0.1, or 127.0.0.11 with ExtServers: [127.0.0.1] and no host( prefix, remove the --dns, compose dns: or daemon.json dns setting and attach the container to a user-defined network, where Docker forwards to the host's own resolvers.

A Docker container usually fails to resolve names because it was pointed at a resolver on a loopback address, 127.0.0.1 or 127.0.0.53, and inside the container that address is the container itself. Docker knows this and rewrites the container's /etc/resolv.conf to work around it, but the workaround is different on each network type, and the two settings people reach for, --dns and the daemon's dns option, switch it off. Run docker exec <name> cat /etc/resolv.conf first. Current engines write comments into that file saying where its nameservers came from, and that is usually the whole diagnosis.

Two answers circulate, and both cost you something#

The first answer, on most forum threads, is "add "dns": ["8.8.8.8", "8.8.4.4"] to /etc/docker/daemon.json and restart". It works, which is why it spreads. What it does is override the nameservers for every container on every network. Containers stop asking your router, your Pi-hole or AdGuard Home, so local names stop resolving inside containers and blocklists stop applying to them. It hides the fault instead of fixing it.

The second answer is "containers use the host's DNS, so if the host resolves, the container does". Docker's own networking overview starts there, "containers use the same DNS servers as the host by default", and then qualifies it at once: containers on the default bridge "receive a copy" of the host's file, and containers on a custom network use Docker's embedded DNS server, which "forwards external DNS lookups to the DNS servers configured on the host". The source shows that neither is a plain copy when the host's resolver lives on loopback.

What Docker writes into the container, measured#

We tested each case on Docker Engine 28.1.1 with Compose 2.35.1, on a host whose /etc/resolv.conf lists two loopback resolvers, a local BIND on 127.0.0.1 and systemd-resolved's stub on 127.0.0.53. That is the same shape as a server running Pi-hole on port 53. The transform functions are unchanged in the current release, docker-v29.9.1.

How the container was startedNameserver in its resolv.confWhere queries goResult
docker run, default bridge8.8.8.8, 8.8.4.4Google, from the containerResolves, but bypasses the host's resolver
User-defined network, or any compose service127.0.0.11Embedded DNS, then 127.0.0.1 and 127.0.0.53 in the host's namespaceResolves through the host's resolver
--network host127.0.0.1, 127.0.0.53Host resolvers directly, file used as isResolves
Default bridge with --dns 127.0.0.1127.0.0.1The container's own loopbackFails: Connection refused
User-defined network with --dns 127.0.0.1 or compose dns: 127.0.0.1127.0.0.11Embedded DNS, then 127.0.0.1 in the container's namespaceFails: SERVFAIL in about 0.5 s
--internal network127.0.0.11Nowhere, forwarding is blockedFails: SERVFAIL

Three rules in Moby's resolvconf package produce that table.

The default bridge strips loopback. The transform for "legacy" networks removes every loopback nameserver, because those "only work in the host's namespace", drops IPv6 nameservers unless IPv6 is enabled, and if nothing is left writes 8.8.8.8 and 8.8.4.4 (plus 2001:4860:4860::8888 and ::8844 with IPv6). The daemon logs No non-localhost DNS nameservers are left in resolv.conf. Using default external servers at info level each time, and the container's file ends with # Used default nameservers.

User-defined networks forward from the host. The container gets only 127.0.0.11. The host's nameservers move into the embedded server's upstream list, marked to be reached "from the host's network namespace". That is why a resolver on the host's 127.0.0.1 works here and fails on the default bridge. The file shows it as # ExtServers: [host(127.0.0.1) host(127.0.0.53)].

An override is reached from the container. Addresses "supplied as overrides are accessed from the container's namespace". --dns, compose dns: and the daemon's dns setting are all overrides: the daemon code passes its own list through the same option as --dns whenever a container sets none. So dns: 127.0.0.1 sends the embedded server to the container's loopback, where nothing listens. The file shows # ExtServers: [127.0.0.1] with no host( prefix. docker run prints WARNING: Localhost DNS (127.0.0.1) may fail in containers. Compose printed no warning in our test.

Read the error: which case are you in#

What you seeCauseFix
nameserver 127.0.0.1 or 127.0.0.53 inside a bridge container, Connection refused or no servers could be reachedA loopback --dns or daemon dns overrideRemove the override, or give a non-loopback address
nameserver 127.0.0.11, ExtServers: [127.0.0.1] without host(, fast SERVFAILSame override on a user-defined networkRemove it; the host's resolver is already used
nameserver 8.8.8.8 and # Used default nameservers.Default bridge, host resolver on loopbackUse a user-defined network
SERVFAIL for external names only, on a network you created with --internalBlocked by design since the CVE-2024-29018 fixAttach a second, non-internal network
NXDOMAIN or bad address for another container's nameDefault bridge has no embedded DNSPut both on a user-defined network
Timeouts with a LAN or public nameserver listedThat server is unreachable from the container, often a firewallSee the firewall section below

The fourth row is a security fix. Moby's advisory for CVE-2024-29018 explains that because dockerd forwards from the host namespace when the host's resolver is on loopback, "internal networks can unexpectedly forward DNS requests to an external nameserver". Releases 26.0.0, 25.0.5 and 23.0.11 stopped forwarding from internal networks. A stack that relied on the old behavior loses external names after the upgrade, and that is intended.

The fifth row is documented plainly: on the default bridge, containers "can only access each other by IP addresses, unless you use the --link option, which is considered legacy". Our test service on network_mode: bridge got NXDOMAIN for a sibling service's name. The compose reference says the same for network_mode: bridge.

Three commands that settle it#

bash
docker exec app cat /etc/resolv.conf           # the comments name the transform
docker exec app nslookup example.com           # SERVFAIL, refused or timeout?
journalctl -u docker --since today | grep -i "non-localhost"

If the image has no nslookup, run the same lookup from a throwaway container attached to the same network: docker run --rm --network <project>_default alpine nslookup example.com. If the file was edited by hand inside the container, Docker has stopped managing it. Its header says so: "Docker Engine will not make further changes once it has been modified."

systemd-resolved and 127.0.0.53#

A host running systemd-resolved in its usual stub mode, the Ubuntu default, has one nameserver line in /etc/resolv.conf: nameserver 127.0.0.53, the local stub. Docker special-cases exactly that: when 127.0.0.53 is the only nameserver, the daemon reads /run/systemd/resolve/resolv.conf instead, the compatibility file systemd-resolved keeps "containing information about all known DNS servers", and logs detected 127.0.0.53 nameserver, assuming systemd-resolved. A second loopback line, as on our test host, turns the detection off.

The check runs once per daemon start. If you switch the host to or from systemd-resolved, or change what /etc/resolv.conf points at, restart Docker before you trust any container's resolver.

Pi-hole or AdGuard Home on the same host#

This is the uncomfortable case. Point the host at its own Pi-hole on 127.0.0.1 and every container on the default bridge quietly asks Google instead. Nothing fails, so nobody notices that those containers skip the blocklists and cannot see the local records you set up in DNS for self-hosters. Containers on user-defined networks, which includes every compose service unless it sets network_mode: bridge, go through the host's resolver correctly.

If a container must use a specific resolver, give it an address it can reach from its own namespace, the host's LAN address, not 127.0.0.1:

yaml
services:
  app:
    dns:
      - 192.168.1.10   # the host running Pi-hole, on its LAN address

Two things then have to be true. The query arrives at the host from a Docker subnet, through the INPUT chain, where UFW's default deny drops it; Reaching a host service from a container has the rule and explains why Docker adds none for this direction. And the resolver has to accept that source. Pi-hole's recommended Local mode answers "only from hosts whose address is on a local subnet, i.e., a subnet for which an interface exists on the server", which covers Docker's bridge subnets when Pi-hole is installed on that host. Its Permit all origins mode does too, but Pi-hole's own docs call it "a dangerous thing to do". For choosing between the two resolvers, see Pi-hole vs AdGuard Home.

When daemon.json dns is the right fix#

When the host's resolvers really are unreachable from containers, for example a host-only resolver on loopback that you want the default bridge to use, or a network that blocks 8.8.8.8. Then set a reachable address, never a loopback one:

json
{
  "dns": ["192.168.1.10", "192.168.1.11"]
}

DNS settings are not among the options the daemon reloads on SIGHUP, so this needs sudo systemctl restart docker. Docker's live restore documentation is plain about the cost: by default, "when the Docker daemon terminates, it shuts down running containers". Check /etc/resolv.conf in a restarted container before you call it fixed.

When this answer stops applying#

  • Docker Desktop on macOS and Windows. Containers run in a virtual machine with its own resolver. Nothing here was tested there.
  • Podman and rootless Docker. Different network back ends; nothing here was measured on either. Docker vs Podman covers how they differ.
  • Names that resolve but connect to the wrong place. That is routing, not DNS. A Docker network that took your LAN's subnet looks exactly like this; Docker subnet clashes and address pools covers it.
  • A published port 53 that will not bind. systemd-resolved holding 127.0.0.53:53 is a port clash, covered in Default port collisions.

What to do next#

Read the container's /etc/resolv.conf, match it to the table above, and remove whichever override put a loopback address there. If you run your own resolver, keep every service on a user-defined network so it actually uses it, and check the host firewall with Why UFW does not block Docker ports open alongside, since Docker's rules and UFW interact in both directions.

Questions#

Why does my container use 8.8.8.8 when my host does not?

The container is on the default bridge network and every nameserver in your host's /etc/resolv.conf is a loopback address, such as a local Pi-hole on 127.0.0.1. Docker removes loopback nameservers for the default bridge because they cannot work from inside the container, and if none are left it writes Google's 8.8.8.8 and 8.8.4.4. The file says so: # Used default nameservers. Put the container on a user-defined network instead.

What is 127.0.0.11 in a Docker container?

It is Docker's embedded DNS server, which every container on a user-defined network, including every compose project's default network, uses as its only nameserver. It answers container and service names itself and forwards everything else to the upstream servers listed in the # ExtServers: comment of the container's /etc/resolv.conf. There is no IPv6 equivalent; the IPv4 address also works in IPv6-only containers.

Should I set dns 8.8.8.8 in daemon.json to fix Docker DNS?

Only if your host's resolvers are unreachable from containers. The setting overrides the nameservers for every container on every network, so containers stop using your router, your Pi-hole or AdGuard Home blocklists, and any local names they serve. It is also not reloadable with SIGHUP, so the daemon needs a full restart. Read the container's /etc/resolv.conf first; the cause is usually a loopback address that a user-defined network fixes.

Why can't containers on an internal Docker network resolve external names?

By design. Since Moby 26.0.0, 25.0.5 and 23.0.11, the embedded DNS server does not forward queries from networks created with --internal, the fix for CVE-2024-29018, where DNS forwarding through the host leaked queries out of networks meant to be isolated. Names of other containers on the same network still resolve. On our test host the external lookup failed with SERVFAIL in under a second.

Does a Docker container see changes I make to its /etc/resolv.conf?

Yes, and from then on Docker leaves the file alone. The header Docker writes says: "This file can be edited; Docker Engine will not make further changes once it has been modified." That makes an in-container edit a trap: it survives until the container is recreated, and it hides any later fix you make on the host or in the compose file.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.