skip to content

How does Docker decide which upstream DNS servers a container uses, and what would you change if every container must query a corporate internal DNS server instead?

level: seniorimportance: should knowfreq 30%

answer

  1. host resolv.conf minus loopback entries
  2. empty after filtering -> public fallback + warning
  3. daemon.json dns / dns-search / dns-opts
  4. --dns, --dns-search, --dns-option, compose dns:
  5. search + ndots = slow or failing short names

basics

~20 s

Docker derives upstream resolvers from the host's /etc/resolv.conf, dropping loopback entries and falling back to public resolvers if nothing is left. Override globally with the daemon.json dns/dns-search/dns-opts keys, or per container with --dns, --dns-search, --dns-option, or Compose's dns keys.

solid answer

~50 s

By default the daemon reads the **host's** `/etc/resolv.conf` and uses those nameservers - either written into the container (default bridge) or used as forwarders behind `127.0.0.11` (user-defined networks). **Loopback nameservers are filtered out**, because `127.0.0.53` (systemd-resolved's stub) is unreachable from a container's namespace; if filtering leaves nothing, Docker falls back to public resolvers and logs a warning. That fallback is the classic cause of "containers ignore our internal DNS and cannot resolve intranet hostnames". To point containers at a corporate resolver: - **Fleet-wide:** set `"dns"`, `"dns-search"` and `"dns-opts"` in `/etc/docker/daemon.json` and restart the daemon. - **Per container:** `--dns 10.0.0.53 --dns-search corp.example.com --dns-option ndots:2`. - **Compose:** the `dns:` and `dns_search:` keys on a service. On systemd-resolved hosts the durable fix is to point the daemon at the *real* upstream servers (visible via `resolvectl status`) rather than the stub. Search domains matter too: a wrong `search` list plus a high `ndots` turns every short name into several failed queries before the right one.

code

bash · 10 lines
bash
cat /etc/docker/daemon.json
# {
#   "dns": ["10.0.0.53", "10.0.1.53"],
#   "dns-search": ["corp.example.com"],
#   "dns-opts": ["ndots:1", "timeout:2"]
# }
sudo systemctl restart docker

docker run --rm alpine cat /etc/resolv.conf
docker run --rm --network app alpine getent hosts git.corp.example.com

go deeper

for a junior

Know that Docker takes DNS servers from the host by default and that --dns can override them for one container.

for a middle

Add the precedence order (per-container flags, daemon.json, host file) and the loopback-filtering rule with its public-resolver fallback.

for a senior

Diagnose split-horizon symptoms, fix at the daemon level for a fleet, and account for search domains, ndots and the need to recreate containers.

for a principal

Treat resolver configuration as host-image policy: bake it into the daemon configuration your provisioning applies, so container images stay environment-agnostic.

## Where the upstream list comes from Containers do not inherit the host's resolver by magic. When the daemon starts a container it builds a DNS configuration in this order of precedence: 1. **Per-container flags** - `--dns`, `--dns-search`, `--dns-option` (Compose: `dns`, `dns_search`). 2. **Daemon configuration** - `dns`, `dns-search`, `dns-opts` in `/etc/docker/daemon.json` (or the equivalent `dockerd` flags). 3. **The host's `/etc/resolv.conf`**, filtered. On the **default bridge** the resulting nameservers are written directly into the container's `/etc/resolv.conf`. On a **user-defined network** the container instead sees `nameserver 127.0.0.11`, and the resolved list becomes the embedded resolver's **forwarders**. Either way the same source list is used. ## The loopback filter, and why it bites Loopback nameservers are stripped from the inherited list. The reason is structural: `127.0.0.53` on a systemd-resolved host, or `127.0.0.1` for a local dnsmasq, refers to the *host's* loopback. A container has its own loopback, so copying that entry would point it at nothing. If every entry is loopback - the normal case on modern Ubuntu - the filter empties the list and Docker falls back to well-known public resolvers, logging a warning. Symptoms: public names resolve, but `git.corp.internal` does not, and split-horizon DNS answers with the wrong (external) address. The correct fixes, in order of durability: - Put the **real** corporate resolvers in `daemon.json` under `"dns"`. - Or point the daemon at the upstream servers systemd-resolved itself is using (`resolvectl status` shows them per link). - Avoid the tempting hack of bind-mounting the host's `/etc/resolv.conf`; you re-import the loopback entry that cannot work. ## Search domains and ndots `search` appends suffixes to short names; `options ndots:N` says "a name with fewer than N dots gets the search list tried first". Docker propagates the host's search list unless you override it. Two failure shapes appear repeatedly: - A long inherited search list plus a high `ndots` means every lookup of a short name issues several queries that must fail (often waiting for timeouts) before the unqualified name is tried. This shows up as slow startup rather than as an error. - Conversely, an application that relies on a short intranet name breaks when the search list is *not* propagated, and needs `--dns-search corp.example.com`. On a user-defined network Docker typically sets `options ndots:0`, so container names are tried as-is first - part of why container-name resolution stays fast even with search domains present. ## Verifying Inspect what the container actually got (`cat /etc/resolv.conf` inside it), then query both an internal and an external name with `getent hosts` to separate forwarding problems from record problems. Check the daemon's effective configuration and its logs for the fallback warning. Remember that changing `daemon.json` requires a daemon restart and only affects **newly created** containers - existing ones keep the configuration they were built with, which is why "we fixed DNS but the container still misbehaves" usually just means it was never recreated. ## Related knobs `--add-host name:ip` injects a static `/etc/hosts` entry, useful for pinning one hostname without touching DNS. Containers on the **host** network use the host's resolv.conf as-is, so none of the filtering or embedded-resolver behaviour applies to them - a detail worth stating explicitly when someone reports that DNS behaves differently for one container.

  • On an Ubuntu host running systemd-resolved, containers resolve public names but not intranet names. What happened?
    The host's `/etc/resolv.conf` lists only the loopback stub `127.0.0.53`. Docker filters loopback nameservers because they are meaningless in a container namespace, is left with an empty list, and falls back to public resolvers - which know nothing about the intranet zone. Fix it by configuring the real upstream servers in `daemon.json` (or per container with `--dns`).
  • You changed `daemon.json` and restarted Docker, but one container still uses the old resolvers. Why?
    A container's DNS configuration is materialised when the container is created, not read live. Existing containers keep the resolver set they were built with, so the change only applies to newly created containers - recreate it. A container on the host network is a separate case: it uses the host's resolv.conf directly.

saying these in an interview costs you the question

  • Believing containers automatically inherit the host's resolv.conf verbatim, loopback entries included
  • Bind-mounting the host /etc/resolv.conf as the fix on a systemd-resolved host
  • Expecting a daemon.json DNS change to affect already-running containers
  • Ignoring search domains and ndots when short intranet names resolve slowly or not at all
  • Assuming host-network containers go through the embedded resolver

context