Your containerized app runs fine, then suddenly it cannot reach api.github.com, your internal database, or anything by hostname. Logs show Name or service not known, getaddrinfo ENOTFOUND, or nodename nor servname provided. Docker container DNS resolution failures are maddening because they mimic application bugs, sometimes vanish when you shell in and test manually, and often appear only in specific network configurations. This guide covers every common root cause — ranked by how often they surface in production — with exact commands and panel paths to fix each one.

How Docker Resolves Hostnames Inside Containers

Before diagnosing, you need a clear picture of what is supposed to happen. Docker's DNS behavior differs by network type, and mixing them up leads to chasing the wrong fix.

  • User-defined networks (created with docker network create or any Compose network): Docker injects nameserver 127.0.0.11 into /etc/resolv.conf inside the container. Queries hit Docker's embedded DNS server running inside the daemon process. It resolves container and service names internally and forwards anything it does not recognize to upstream resolvers discovered from the host.
  • Default bridge network (docker0): Docker copies the host's /etc/resolv.conf verbatim into the container. Docker's embedded DNS server is not involved for external queries — the container queries whatever nameservers the host uses directly.
  • Host network mode: The container shares the host's entire network stack. DNS works exactly like the host — which means it breaks exactly like the host too.

Knowing which network type a container uses immediately cuts the suspect list. Check it with:

docker inspect <container_name_or_id> | grep -i networkmode

Root Causes, Ranked by Frequency

1. systemd-resolved Stub Listener on the Host

This is the single most common cause of broken DNS in Docker containers on modern Linux, and it catches almost every engineer who sets up Docker on Ubuntu 20.04 or later without checking the DNS stack first.

systemd-resolved binds a local stub resolver at 127.0.0.53 and writes that address into /etc/resolv.conf — either directly or via a symlink to /run/systemd/resolve/stub-resolv.conf. When Docker starts a container on the default bridge network and copies that file, the container sees nameserver 127.0.0.53. Inside the container, 127.0.0.53 resolves to the container's own loopback interface. Nothing is listening there. Every DNS query times out silently.

Verify this is your situation:

cat /etc/resolv.conf # If you see nameserver 127.0.0.53, you have the stub conflict ls -la /etc/resolv.conf # Confirm whether it is a symlink and which file it points to

The cleanest fix is to configure Docker's daemon to use explicit upstream resolvers, bypassing the host resolver entirely:

sudo nano /etc/docker/daemon.json

Add or merge:

{ "dns": ["1.1.1.1", "8.8.8.8"] }
sudo systemctl restart docker

The alternative fix is to re-point /etc/resolv.conf at the real (non-stub) resolved config:

sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf

Unlike the stub file, /run/systemd/resolve/resolv.conf lists real upstream nameserver IPs that are reachable from inside a container. Note that this only matters for the default bridge network — user-defined networks always use 127.0.0.11 regardless of what the host's resolv.conf says.

2. No Explicit DNS Set in daemon.json

Docker's embedded DNS at 127.0.0.11 discovers upstream resolvers from the host at daemon start. If the host resolver is broken, loopback-only, or filtered by a corporate firewall that blocks queries from bridge subnet IPs, Docker has no fallback and all external resolution fails. Setting resolvers explicitly in daemon.json short-circuits host discovery:

sudo nano /etc/docker/daemon.json { "dns": ["1.1.1.1", "8.8.8.8"], "dns-search": [], "dns-opts": ["ndots:1"] }

The ndots:1 setting prevents containers from prepending search domain suffixes on every short hostname before attempting the fully qualified query. With a long search domain list and the default ndots:5, each DNS lookup can trigger five failed queries before the real one — adding 5–10 seconds of latency that looks exactly like an application timeout.

3. Host Firewall Blocking Port 53 from the Bridge Interface

Docker automatically inserts iptables rules to allow container traffic to reach the host and the internet. However, some host firewalls — particularly UFW on Ubuntu — have a DOCKER-USER chain that blocks traffic originating from bridge subnets (172.17.0.0/16 for the default bridge, or whatever range you have configured) before Docker's own rules can act on it. Containers on user-defined networks can reach the embedded DNS at 127.0.0.11, but that server cannot forward to upstream resolvers because outbound UDP and TCP on port 53 from the bridge is dropped.

# Check iptables for DOCKER-USER rules sudo iptables -L DOCKER-USER -n -v # Check UFW status sudo ufw status verbose

Allow DNS from Docker subnets through UFW:

sudo ufw allow from 172.16.0.0/12 to any port 53 proto udp sudo ufw allow from 172.16.0.0/12 to any port 53 proto tcp sudo ufw reload

On nftables-managed hosts (Debian 12, RHEL 9, Fedora 37 and later), inspect the forward chain:

sudo nft list ruleset | grep -A 10 "chain forward"

4. VPN Split Tunneling or Corporate DNS Interception

When a VPN client is active on the host, it often installs a custom nameserver and routes all port-53 traffic through the encrypted tunnel. Docker's embedded DNS tries to forward container queries to that nameserver, but the queries arrive with the host's bridge IP as the source address. The VPN provider's resolver refuses or silently drops them because they did not originate from inside the tunnel. The result is that DNS works perfectly from the host but fails entirely from containers.

Quick diagnosis: disconnect the VPN, restart an affected container, and test DNS. If resolution works, the VPN is the cause. Solutions in order of preference:

  • Add the VPN's internal DNS servers explicitly to daemon.json alongside public fallback resolvers.
  • Configure the VPN client to exclude Docker subnets from the tunnel using split-tunnel exclusion rules (available in GlobalProtect, Cisco Secure Client, Tailscale, and most enterprise clients).
  • Set a per-service DNS override in Compose that points directly at a resolver the VPN allows, such as the corporate DNS server's IP inside the tunnel subnet.

5. IPv6 Mismatches and AAAA Lookup Failures

If your application makes AAAA queries and Docker's embedded DNS has IPv6 disabled — or the host has broken IPv6 routing — you will see intermittent failures for dual-stack public hosts. Many APIs return AAAA records preferentially, and a failed AAAA lookup stalls the connection for several seconds before falling back to A, which surfaces as random timeout errors rather than obvious DNS failures.

Enable IPv6 in the Docker daemon:

{ "ipv6": true, "fixed-cidr-v6": "fd00::/80" }

Or suppress AAAA lookups for a specific container or service:

# docker run docker run --dns-opt="single-request" your-image # docker-compose.yml services: app: image: my-image dns_opt: - single-request

6. Container Image Overwriting resolv.conf at Build Time

Some base images ship with a hard-coded /etc/resolv.conf baked into an image layer. If a Dockerfile writes to that file during build, Docker's runtime injection is silently overridden. The container uses whatever was baked in, which breaks on networks that filter specific public nameservers.

docker run --rm your-image cat /etc/resolv.conf

If the output does not show 127.0.0.11 or your daemon's configured resolvers, the image is overriding the injection. Fix it by removing any Dockerfile commands that write to /etc/resolv.conf — that file should never be touched during a build.

Step-by-Step Diagnostic Sequence

Work through this in order. Stop at the first failure point — that is your root cause.

  1. Shell into the container: docker exec -it <container> sh
  2. Read what resolver the container sees: cat /etc/resolv.conf
  3. Test TCP/UDP connectivity to that resolver on port 53: nc -zv 127.0.0.11 53
  4. Test DNS via the container's configured resolver: nslookup google.com 127.0.0.11
  5. Test with a known-good external resolver: nslookup google.com 1.1.1.1
  6. If step 4 fails but step 5 works, the embedded DNS server or its upstream forwarding path is broken (root causes 1–4).
  7. If both steps 4 and 5 fail, the container has no routable path to port 53 at all — check iptables, nftables, or the image.
# Full diagnostic sequence from inside the container cat /etc/resolv.conf # Confirm port 53 is reachable nc -zv 127.0.0.11 53 2>&1 # Test resolution via embedded DNS nslookup google.com 127.0.0.11 # Test resolution via external DNS (bypasses embedded server) nslookup google.com 1.1.1.1 # If dig is available (apt-get install -y dnsutils) dig @127.0.0.11 google.com dig @1.1.1.1 google.com # Minimal test without any DNS tools wget -q --spider https://google.com 2>&1 | head -3
💡 Not sure whether the problem is inside your Docker setup or whether upstream nameservers have the wrong records? Use the DNS Propagation Checker to verify your authoritative records from multiple global resolvers before debugging the Docker layer.

Platform-Specific Differences

Docker Desktop on macOS and Windows

Docker Desktop runs Docker Engine inside a Linux VM managed by the hypervisor (Apple Virtualization Framework on Apple Silicon, Hyper-V or WSL2 on Windows). DNS is several layers deep: the Linux VM inherits DNS from the hypervisor, which reads it from the host OS's system DNS settings.

  • On macOS: System Settings → Network → [interface] → Details → DNS
  • On Windows: Control Panel → Network and Sharing Center → adapter properties → Internet Protocol Version 4 → DNS server addresses
  • When a VPN is active on the host, the VM's DNS can break. Override it in Docker Desktop via Settings → Docker Engine — same daemon.json format, click Apply and Restart after saving. There is no file to edit directly; the JSON is applied to the VM.
  • The systemd-resolved conflict does not exist on Docker Desktop because the Linux VM does not use systemd-resolved in the same way as an Ubuntu server installation.

Docker Engine on Linux (Production Servers)

Configuration lives at /etc/docker/daemon.json. After every change:

sudo systemctl daemon-reload sudo systemctl restart docker # Confirm the daemon loaded your DNS configuration docker info | grep -A 5 "DNS"

On systems running Docker with rootless mode enabled, the daemon.json path changes to ~/.config/docker/daemon.json and the service is managed per-user.

Kubernetes with Docker as Runtime

If Docker is the container runtime in a Kubernetes cluster, DNS is handled by CoreDNS (or kube-dns in older clusters), not Docker's embedded server. Container /etc/resolv.conf points to the cluster DNS service IP set by the kubelet. Docker daemon.json DNS settings are irrelevant here — investigate the CoreDNS ConfigMap and pod DNS policy (dnsPolicy and dnsConfig in the pod spec) instead.

Per-Service DNS Override in Docker Compose

You can set DNS per-service without touching daemon.json, which is useful when different services need different resolvers — for example, one service resolving internal corporate hostnames via a private DNS while another uses only public resolvers:

services: api: image: my-api dns: - 1.1.1.1 - 8.8.8.8 dns_search: - internal.company.com dns_opt: - ndots:1 - timeout:2 - attempts:3 worker: image: my-worker dns: - 10.10.0.5 - 1.1.1.1
💡 After updating daemon.json or Compose DNS settings, run a spot-check of your domain's authoritative records using the DNS Lookup tool — if your records are wrong globally, the fix is upstream of Docker entirely.

Verifying the Fix Worked

# Step 1: confirm daemon settings took effect docker info | grep -A 5 DNS # Step 2: check what gets injected into a fresh container docker run --rm busybox cat /etc/resolv.conf # Step 3: test A record resolution docker run --rm busybox nslookup github.com # Step 4: test AAAA (IPv6) resolution docker run --rm tutum/dnsutils dig AAAA github.com # Step 5: confirm service-name resolution (user-defined networks only) # From inside a running container, try to resolve a peer service name docker exec -it api_container nslookup db

Common Misdiagnoses

"It worked yesterday." DNS that worked before can break after a host OS update — Ubuntu upgrades frequently reset the systemd-resolved symlink — after a VPN client update that installs a new resolver, or after a Docker version upgrade that changes default bridge behavior. Treat it as a fresh investigation, not a regression hunt.

"It works when I run the container manually but not via Compose." docker run and docker compose up can attach containers to different networks. A manually run container lands on the default bridge while a Compose service lands on a user-defined Compose network — or vice versa — with completely different DNS resolution paths. Confirm the network with docker inspect on both containers.

"nslookup works inside the container but the app cannot connect." If nslookup resolves correctly but the application cannot reach the host, DNS is not the problem. Check: TLS certificate validation errors, wrong port, the HTTP_PROXY or HTTPS_PROXY environment variable interfering with direct connections, or whether the app uses a bundled resolver that bypasses /etc/resolv.conf entirely (some Node.js runtimes and Go binaries do this in specific configurations).

"Ping works, HTTP does not." Ping resolves hostnames, so a successful ping confirms DNS is working. If the hostname resolves but HTTP fails, the issue is TLS, port filtering, a firewall dropping TCP connections on port 80 or 443, or application configuration — not DNS.

Preventing Recurrence

  • Always set explicit DNS in /etc/docker/daemon.json on Linux hosts. Do not rely on Docker auto-discovering resolvers from the host — it works until the host changes its DNS stack.
  • Pin ndots:1 in daemon config or per-service Compose config to eliminate lookup delays from search domain expansion.
  • After every host OS upgrade, re-check ls -la /etc/resolv.conf — Ubuntu in particular sometimes resets the symlink.
  • Add a DNS health check to long-running services: test: ["CMD", "nslookup", "google.com"] in the Compose healthcheck block catches DNS failures before they cause upstream alerts.
  • Version-control daemon.json alongside your infrastructure code (Ansible, Terraform, or a simple dotfiles repo) so changes are tracked and reproducible.

2026: DoH, DoT, DNSSEC, and What They Break in Docker

DNS-over-HTTPS and DNS-over-TLS are increasingly standard on host systems. Fedora 40 and later enable DoT in systemd-resolved by default. macOS Sequoia's system resolver supports DoH natively. These do not usually break Docker DNS resolution directly, but they introduce a new failure mode: the host resolver validates DNSSEC, a DNS zone has a broken DNSSEC chain, and every query for that domain fails with SERVFAIL — silently, with no retry — instead of returning a degraded but usable answer.

If you see resolution failures only for specific domains while others work fine, and your host runs a validating resolver, isolate DNSSEC as the cause:

# Check if the host resolver validates DNSSEC resolvectl status # Look for DNSSEC: yes # Test a domain with DNSSEC validation enabled (default) dig +dnssec yourdomain.com # Repeat with validation disabled — if this works, the zone has a broken DNSSEC chain dig +cd yourdomain.com # From inside a container docker run --rm tutum/dnsutils dig +cd yourdomain.com

For containerized environments, pointing daemon.json at resolvers that perform DNSSEC validation transparently — Cloudflare 1.1.1.1 or Google 8.8.8.8 — is safer than inheriting host-side validation behavior. Those resolvers return structured SERVFAIL responses with extended error codes when a zone's DNSSEC is broken, rather than silently dropping the query. See RFC 8305 (Happy Eyeballs v2) for how modern dual-stack applications are expected to handle the A/AAAA resolution race — understanding it helps distinguish a Docker network configuration problem from an application-level DNS handling issue that looks identical from the outside.

On dual-stack hosts in 2026, verify that enabling IPv6 in Docker daemon.json actually plumbs IPv6 routes through to user-defined networks. Run docker network inspect <network_name> and confirm an IPv6 subnet appears under the IPAM block. If you enabled IPv6 in daemon.json but the network predates that change, recreate the network — existing networks do not retroactively gain IPv6 when the daemon setting is added.