Docker & ContainersXXVI Β· DNS & Service DiscoveryResolver configuration
Building the resolver β where a container gets /etc/resolv.conf
What you'll learn
- Explain the two different ways Docker generates a container resolv.conf
- Order the precedence chain from --dns down to the host resolv.conf
- Diagnose the systemd-resolved loopback trap on the default bridge
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-11
Two containers, same image, same host, same command. One resolves
api and the other does not. Nothing about the image changed. What
changed is the file Docker wrote into the container at start time,
and Docker writes that file two completely different ways depending
on which network the container joined.
/etc/resolv.conf inside a container is not the hostβs file, is not
in the image, and is not editable in any way that survives a restart.
It is generated. Knowing how it is generated is most of DNS
troubleshooting.
Two generation paths
The Docker documentation states the split plainly: containers that
attach to the default bridge network receive a copy of the hostβs
/etc/resolv.conf, while containers that attach to a custom network
use Dockerβs embedded DNS server.
Default bridge | User-defined network | |
|---|---|---|
nameserver line | copied from host, loopback entries stripped | 127.0.0.11 |
| Service-name resolution | none | container names and aliases |
| Upstream forwarding | container talks to upstream directly | embedded DNS forwards on the containerβs behalf |
| Search domains | copied from host | copied from host |
That table explains most βit works in Compose but not with plain
docker runβ reports. Compose creates a user-defined network for
every project; a bare docker run with no --network lands on the
default bridge.
$ docker exec web cat /etc/resolv.confnameserver 127.0.0.11
options ndots:0Illustrative output
$ docker exec legacy cat /etc/resolv.confnameserver 192.0.2.53
nameserver 192.0.2.54
search corp.example.comIllustrative output
Same host. Different file. The second container has no 127.0.0.11
at all, which is why getent hosts api returns nothing there no
matter how many containers are called api.
The precedence chain
When Docker builds the resolver configuration it walks a fixed order. The first source that supplies a value wins:
- Per-container flags β
--dns,--dns-search,--dns-optionondocker run/docker create, ordns:,dns_search:,dns_opt:in a Compose service. - Network driver options β set when the network is created,
for example
com.docker.network.bridge.dns_serverson a bridge network. - Daemon defaults β
dns,dns-searchanddns-optsin/etc/docker/daemon.json, or the equivalentdockerd --dnsflags. - The hostβs
/etc/resolv.confβ the fallback, with loopback nameservers removed.
{
"dns": ["192.0.2.53", "192.0.2.54"],
"dns-search": ["corp.example.com"],
"dns-opts": ["timeout:2", "attempts:2"]
}A daemon-level dns setting applies to containers on the default
bridge and becomes the upstream that the embedded DNS forwards to
on user-defined networks. It is the right lever when every container
on a host must use a corporate resolver; a per-container --dns is
the right lever when one container must not.
The loopback trap
On a host running systemd-resolved, /etc/resolv.conf usually
contains exactly one nameserver:
$ cat /etc/resolv.confnameserver 127.0.0.53
options edns0 trust-ad
search corp.example.comIllustrative output
127.0.0.53 is the systemd-resolved stub listener. It is bound to
the hostβs loopback interface. A container has its own network
namespace and therefore its own loopback: inside the container,
127.0.0.53 is the container itself, where nothing is listening.
Docker knows this. It strips loopback nameservers when copying the file, and if that leaves nothing it substitutes public resolvers and says so in the daemon log:
$ journalctl -u docker --since '1 hour ago' | grep -i resolvlevel=warning msg="Local (127.0.0.1) DNS resolver found in resolv.conf and containers can't use it. Using default external servers: [nameserver 8.8.8.8 nameserver 8.8.4.4]"Illustrative output
This is the single most common cause of βthe container can reach the
internet but cannot resolve internal namesβ. The host resolves
db.corp.example.com perfectly, because the host talks to
systemd-resolved, which knows about the corporate zone. The
container has been silently handed a public resolver that has never
heard of corp.example.com.
The fix is to give the daemon a real address rather than letting it
guess. Either point daemon.json at the corporate resolvers, or read
the real upstreams out of systemd-resolved and use those:
$ resolvectl status | grep -A2 'Current DNS Server'Current DNS Server: 192.0.2.53
DNS Servers: 192.0.2.53 192.0.2.54
DNS Domain: corp.example.comIllustrative output
On a user-defined network the trap does not bite in the same way, because the embedded DNS forwards from the hostβs namespace rather than the containerβs β but only if the daemon was given a usable upstream to forward to in the first place.
/etc/hosts is a separate file with separate rules
DNS is not the only name source. Docker also generates /etc/hosts,
and entries there are consulted before any resolver is contacted.
docker run --rm \
--add-host legacy-billing:192.0.2.40 \
--add-host host.docker.internal:host-gateway \
alpine:3.20 getent hosts legacy-billingThe documentation is explicit that custom hosts defined on the host
machine are not inherited by containers β --add-host is the only
way in. This matters during migrations: a name pinned in the hostβs
/etc/hosts works for every process on the machine except the
containers.
Inspecting what a container actually got
CTR=web
# 1. Which networks is it on? Default bridge or user-defined?
docker inspect "$CTR" --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}'
# 2. What resolver config did Docker write?
docker exec "$CTR" cat /etc/resolv.conf
# 3. What static names bypass DNS entirely?
docker exec "$CTR" cat /etc/hosts
# 4. What did the container ask for, and what came back?
docker exec "$CTR" getent hosts apiAnswer those four in order and you have located the fault before
reaching for tcpdump. Step 1 tells you which generation path was
used; step 2 tells you whether the file matches that path; steps 3
and 4 tell you whether the name is resolvable at all.
Sanity check
Knowledge check Β· 4 questions
Q1. A container on a user-defined bridge network has `nameserver 127.0.0.11` in /etc/resolv.conf. Where does a query for a public name such as example.com actually go?
Q2. The host runs systemd-resolved, so /etc/resolv.conf on the host contains only `nameserver 127.0.0.53`. What does a container on the default bridge network receive?
Q3. Which of these are legitimate places to configure DNS for containers, in the sense that the setting survives a container restart? Select all that apply.
Q4. Host entries added to the host machine /etc/hosts are inherited by containers running on that host.
Passing score: 75%. Answers are checked in this browser.