Skip to main content
RunBook Academy

Docker & ContainersXXVI Β· DNS & Service DiscoveryResolver configuration

Building the resolver β€” where a container gets /etc/resolv.conf

Intermediate⏱ ~20 min

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

Not yet marked complete on this device.

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 bridgeUser-defined network
nameserver linecopied from host, loopback entries stripped127.0.0.11
Service-name resolutionnonecontainer names and aliases
Upstream forwardingcontainer talks to upstream directlyembedded DNS forwards on the container’s behalf
Search domainscopied from hostcopied 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.

Read-only / Safeuser-defined network
$ docker exec web cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0

Illustrative output

Read-only / Safedefault bridge
$ docker exec legacy cat /etc/resolv.conf
nameserver 192.0.2.53
nameserver 192.0.2.54
search corp.example.com

Illustrative 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:

  1. Per-container flags β€” --dns, --dns-search, --dns-option on docker run / docker create, or dns:, dns_search:, dns_opt: in a Compose service.
  2. Network driver options β€” set when the network is created, for example com.docker.network.bridge.dns_servers on a bridge network.
  3. Daemon defaults β€” dns, dns-search and dns-opts in /etc/docker/daemon.json, or the equivalent dockerd --dns flags.
  4. The host’s /etc/resolv.conf β€” the fallback, with loopback nameservers removed.
Configuration change/etc/docker/daemon.json
{
"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:

Read-only / Safehost
$ cat /etc/resolv.conf
nameserver 127.0.0.53
options edns0 trust-ad
search corp.example.com

Illustrative 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:

Read-only / Safejournalctl -u docker
$ journalctl -u docker --since '1 hour ago' | grep -i resolv
level=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:

Read-only / Safehost
$ 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.com

Illustrative 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.

Configuration changedocker run
docker run --rm \
--add-host legacy-billing:192.0.2.40 \
--add-host host.docker.internal:host-gateway \
alpine:3.20 getent hosts legacy-billing

The 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

Read-only / Safediagnosis
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 api

Answer 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

  1. 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?

  2. 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?

  3. 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.

  4. 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.