You changed your Wi-Fi, plugged into a dock, or fired up a VPN, and now every apt update or pip install inside your container dies with Temporary failure in name resolution. The host resolves fine. The container doesn't. Classic.
I had a client last month whose entire print queue died because their Docker-based print spooler couldn't resolve the label printer's hostname after they moved a laptop between two VLANs. Same error, same cause. Here's what actually fixes it.
Cause 1: Docker daemon is still holding the old DNS config
This is the big one. Docker builds its embedded DNS (the one listening on 127.0.0.11 inside user-defined networks) from whatever resolvers the daemon saw when it started. If you were on 192.168.1.1 for DNS and now you're on a VPN that hands out 10.8.0.1, Docker doesn't notice. It keeps forwarding to a resolver that's now unreachable or filtered.
You can confirm it fast:
docker run --rm alpine cat /etc/resolv.conf
# nameserver 192.168.1.1 <-- stale
docker info | grep -A2 "Server Version"
docker info | grep -i dns
If the nameserver listed is something you can't ping from the host anymore, that's your problem.
The fix
Restart the daemon so it re-reads the current resolver state:
sudo systemctl restart docker
On Docker Desktop (Mac/Windows), quit it from the tray and start it again. The whole VM gets rebuilt and picks up the new DNS.
If you want it to stay fixed across network changes, pin explicit resolvers in /etc/docker/daemon.json:
{
"dns": ["1.1.1.1", "8.8.8.8"],
"dns-search": ["yourdomain.local"]
}
Then reload:
sudo systemctl restart docker
Don't put your router's IP in there. It changes. Public resolvers don't. If you need internal names, list both the internal DNS server and a public fallback, in that order.
Cause 2: Broken iptables NAT rules from the old network
Docker inserts NAT rules into the nat table so container traffic can reach the outside world, plus a rule in filter that allows DNS to 127.0.0.11. When your host's default route changes, sometimes the old MASQUERADE rules point at an interface that no longer exists (like wlan0 disappearing when you plug in Ethernet). The container sends the packet, it goes nowhere, and glibc reports the resolution failure.
Check what interface Docker thinks it's using:
sudo iptables -t nat -L POSTROUTING -n -v | grep -i masq
# MASQUERADE all -- 172.17.0.0/16 !docker0 <-- missing -o eth0
If you see stale references to an interface that's down, that's it.
The fix
The cleanest way is to let Docker rebuild its rules. On Ubuntu/Debian with ufw you'll also want to reset that:
sudo systemctl stop docker
sudo iptables -t nat -F
sudo iptables -t nat -X
sudo iptables -F DOCKER
sudo iptables -F DOCKER-USER
sudo systemctl start docker
# if ufw is in play
sudo ufw reload
Verify the MASQUERADE rule now names a live interface:
sudo iptables -t nat -L POSTROUTING -n -v | grep MASQUERADE
Users running firewalld (Fedora, RHEL, CentOS Stream) should install the Docker firewalld plugin instead of hand-patching rules — Docker's docs cover this and it stops the whole class of breakage.
Cause 3: systemd-resolved stub listener conflict
On Ubuntu 20.04/22.04/24.04 and Fedora, systemd-resolved binds 127.0.0.53:53 and your host's /etc/resolv.conf is a symlink to /run/systemd/resolve/stub-resolv.conf. Docker copies that file into the container at start. If the symlink gets clobbered — NetworkManager sometimes rewrites it, VPN clients love to — the container ends up with a nameserver it can't reach from inside its network namespace.
Verify:
ls -l /etc/resolv.conf
# lrwxrwxrwx ... /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf <-- good
# -rw-r--r-- ... <-- bad, some tool overwrote it
resolvectl status | head -20
The fix
Restore the symlink and re-read DNS:
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
sudo resolvectl flush-caches
sudo resolvectl reset-server-features
sudo systemctl restart docker
If a VPN client is the culprit (Twingate, Tailscale, some enterprise ZTNA agents), check whether it's writing /etc/resolv.conf directly. The real fix there is a daemon.json dns entry that explicitly points at your VPN's resolver — don't rely on Docker inheriting it.
Quick-reference table
| Symptom | Cause | Fix |
|---|---|---|
Container /etc/resolv.conf has stale nameserver, host resolves fine |
Daemon holds old resolver | sudo systemctl restart docker and set dns in daemon.json |
| DNS fails only after switching Wi-Fi/Ethernet/VPN | Broken iptables NAT rules | Flush nat and DOCKER chains, restart dockerd |
/etc/resolv.conf is a regular file, resolvectl misbehaves |
systemd-resolved symlink clobbered | Recreate symlink to stub-resolv.conf, flush caches |
| Only some containers affected | Custom bridge with bad subnet overlap | Change subnet in docker network create --subnet, recreate network |
| Works with IP, not hostname | Container on default bridge without DNS |
Move to a user-defined network so embedded DNS at 127.0.0.11 is used |
One more thing that bites people: if you're on Docker Desktop for Windows with WSL2, restarting Docker Desktop isn't always enough after a VPN change. Run wsl --shutdown from PowerShell, then relaunch Docker Desktop. The WSL VM caches its own resolver state and won't pick up the new one otherwise.
If you've done all three and it's still broken, run this from inside a broken container to see exactly where it's dying:
docker run --rm --dns 1.1.1.1 alpine sh -c "apk add --no-cache bind-tools && dig +short google.com @1.1.1.1"
If that works but the default doesn't, DNS is fine — your container's resolver config isn't. That narrows it to Cause 1 almost every time.