Resolving Docker DNS lookup failure inside container on custom bridge network

Root Cause Diagnosis

Containers on a user-defined bridge network send all DNS queries to Docker’s embedded resolver at 127.0.0.11, which answers container names itself and forwards everything else to upstream servers taken from the host’s /etc/resolv.conf. The lookup fails when that forwarding path breaks: the host lists only a loopback stub resolver Docker cannot use (such as 127.0.0.53), the upstream servers are unreachable from the bridge subnet, or the host firewall drops DNS traffic leaving the bridge interface (br-<id>). The default bridge network does not use the embedded resolver and copies the host’s resolver settings instead, which is why the same image can work there and fail on a custom network.

Quick Fix

Force explicit upstream resolvers for the container:

docker run --rm --network mynet --dns 1.1.1.1 --dns 8.8.8.8 alpine nslookup example.com

For Compose:

services:
  app:
    image: myapp
    networks: [mynet]
    dns:
      - 1.1.1.1
      - 8.8.8.8

Make it global for all containers by setting /etc/docker/daemon.json:

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

Existing containers must be recreated to pick this up.

Step-by-Step Resolution

1. Confirm the symptom and the resolver in use

docker run --rm --network mynet alpine sh -c 'cat /etc/resolv.conf; nslookup example.com'

Expected on a custom network: nameserver 127.0.0.11. A timeout or SERVFAIL here confirms the embedded resolver is not getting answers upstream.

2. Separate internal resolution from external resolution

# Container-to-container name (handled by Docker itself)
docker run --rm --network mynet alpine nslookup <other-container-name>

# External name via the embedded resolver
docker run --rm --network mynet alpine nslookup example.com

# External name bypassing the embedded resolver
docker run --rm --network mynet alpine nslookup example.com 8.8.8.8
InternalEmbedded externalDirect 8.8.8.8Meaning
OKFailsOKUpstream forwarding or host resolver issue (steps 3–4)
OKFailsFailsFirewall, forwarding, or MTU problem (steps 5–7)
Failsn/an/aWrong network, missing alias, or container not attached (docker network inspect mynet)

3. Inspect the host resolver configuration

cat /etc/resolv.conf
resolvectl status

If /etc/resolv.conf lists only 127.0.0.53, Docker falls back to /run/systemd/resolve/resolv.conf. If that file is missing or empty, Docker falls back to Google DNS (8.8.8.8, 8.8.4.4), which fails on networks that block outbound DNS to third parties. Verify the real upstream servers:

cat /run/systemd/resolve/resolv.conf

4. Pin upstream resolvers

Set dns in /etc/docker/daemon.json to servers actually reachable from the host (your corporate or ISP resolver, or a public one if permitted), then restart Docker:

sudo systemctl restart docker

A per-container --dns or Compose dns: overrides the daemon-level setting.

See also  Fixing apt-get update Hash Sum Mismatch Error on Ubuntu Archive Mirrors

5. Verify IP forwarding and Docker’s iptables management

sysctl net.ipv4.ip_forward

If it returns 0, enable it persistently in /etc/sysctl.d/99-docker.conf:

net.ipv4.ip_forward = 1
sudo sysctl --system

Also check /etc/docker/daemon.json for "iptables": false, which stops Docker from creating the masquerade and forwarding rules that DNS replies depend on. Remove it unless you manage those rules manually.

6. Check the firewall for dropped DNS traffic

sudo iptables -S DOCKER-USER
sudo iptables -t nat -S POSTROUTING | grep -i masq
sudo iptables -S FORWARD | head

Look for DROP or REJECT rules matching UDP/TCP port 53 or the bridge subnet. Find the bridge interface name with:

docker network inspect mynet -f '{{.Id}}' | cut -c1-12   # interface is br-<this id>

firewalld hosts: place the Docker bridge interfaces in a zone that permits forwarding, then restart Docker (a firewall-cmd --reload wipes Docker’s chains):

sudo firewall-cmd --permanent --zone=trusted --add-interface=br-<id>
sudo firewall-cmd --reload
sudo systemctl restart docker

ufw hosts: ensure DEFAULT_FORWARD_POLICY="ACCEPT" in /etc/default/ufw, or add an explicit rule such as sudo ufw route allow in on br-<id>.

7. Rule out MTU mismatch

Large DNS responses (truncated UDP, DNSSEC, TCP fallback) fail silently when the path MTU is below Docker’s default of 1500, which is common on VPNs, cloud overlays (1450 or lower) and PPPoE.

ip link show | grep -E 'mtu' | head

Set it for all new networks in /etc/docker/daemon.json:

{
  "mtu": 1400
}

Or per network:

docker network create --opt com.docker.network.driver.mtu=1400 mynet

8. Recreate the network and containers, then retest

docker compose down
docker network rm mynet   # only if created manually
docker compose up -d
docker exec <container> nslookup example.com

Why This Fails for Some Users

  • Snap-installed Docker: The snap sandbox cannot read /run/systemd/resolve/resolv.conf, so the systemd-resolved fallback fails and only Google DNS is used. Set dns explicitly in /var/snap/docker/current/config/daemon.json, or switch to the apt or official Docker package.
  • VPN with split DNS: Internal domains resolve only through the VPN’s resolver, which the host exposes via resolvectl per-link settings, not /etc/resolv.conf. Docker does not inherit per-link routing, so set dns to the VPN’s resolver and dns-search to the internal domain.
  • WSL2: /etc/resolv.conf is auto-generated at each start and may point to an unreachable 172.x.x.1 gateway. Set generateResolvConf = false under [network] in /etc/wsl.conf and write a static resolver file, or set dns in the Docker daemon config.
  • Rootless Docker: DNS is forwarded through the user-space network stack (slirp4netns or pasta), not the host bridge, so the br-<id> firewall steps do not apply. Configure dns in ~/.config/docker/daemon.json, then run systemctl --user restart docker.
  • iptables-legacy vs iptables-nft mismatch (Debian 11+, Ubuntu 22.04+, RHEL 9): If Docker and another tool write to different backends, forwarding rules can exist but never be evaluated. Compare iptables --version with update-alternatives --display iptables and make Docker and the host firewall use the same backend.
  • musl-based images (Alpine): Older musl versions (before 1.2.4) do not retry over TCP on truncated responses and query all nameservers in parallel, so behavior differs from glibc images on the same network. Test with a Debian-based image to confirm.
  • Containers started with --network host or a custom --dns in docker run: These bypass or override the embedded resolver, so daemon-level dns changes have no effect on them.
  • Bind-mounted /etc/resolv.conf: A volume mount over this file in a container overrides Docker’s generated version, including the 127.0.0.11 entry. Remove the mount.
  • Cloud security groups and network ACLs: Outbound UDP/TCP 53 to arbitrary resolvers may be blocked at the VPC level. Use only the provider’s resolver (for example 169.254.169.253 on AWS or 169.254.169.254 on GCP).
  • IPv6-only upstream resolvers: If the host’s only listed resolver is IPv6 and the bridge has no IPv6 enabled, forwarding fails. Add an IPv4 resolver to dns or enable IPv6 on the daemon ("ipv6": true with fixed-cidr-v6).
See also  Linux out of memory killer (oom-killer) picking wrong high-priority daemon: Tuning oom_score_adj

Leave a Comment