DNS-resolutieproblemen debuggen in Docker-containers (gevorderd)
DNS-problemen in Docker zijn zelden “alleen DNS”. Vaak is het een combinatie van: Docker’s embedded DNS (127.0.0.11), de resolv.conf die in de container terechtkomt, iptables/nftables NAT-regels, MTU/fragmentatie, split-horizon DNS in je bedrijfsnetwerk, VPN-clients die /etc/resolv.conf herschrijven, of zelfs applicaties die eigen caching/Happy Eyeballs/IPv6-voorkeuren hebben. In deze tutorial leer je systematisch en reproduceerbaar debuggen, met echte commando’s en interpretatie van output.
Inhoud
- Mentale model: hoe DNS werkt in Docker
- Symptomen classificeren
- Snelle triage: 60 seconden checklist
- Inspecteer de container: resolv.conf, nsswitch, libc
- Docker embedded DNS (127.0.0.11) en user-defined networks
- Host vs container: vergelijk resolutiepad
- Packet capture: tcpdump op host én in container
- iptables/nftables en forwarding/NAT
- MTU, fragmentatie en EDNS0
- IPv6, Happy Eyeballs en AAAA-problemen
- VPN’s, systemd-resolved, NetworkManager en split DNS
- Kubernetes-achtige patronen in “gewone” Docker setups
- Oplossingsstrategieën (met trade-offs)
- Reproduceerbare testmatrix en afsluitende tips
Mentale model: hoe DNS werkt in Docker
1) Wat gebeurt er bij getaddrinfo() in een container?
De meeste applicaties gebruiken getaddrinfo() (glibc/musl). Die volgt grofweg:
- Lees
/etc/nsswitch.conf(bijv.hosts: files dns). - Check
/etc/hosts. - Raadpleeg DNS via resolvers uit
/etc/resolv.conf. - Respecteer opties zoals
ndots,timeout,attempts,rotate,search.
In Docker-containers wijst /etc/resolv.conf vaak naar:
nameserver 127.0.0.11(Docker’s embedded DNS) op user-defined bridge networks.- Of direct naar de host nameservers (bijv.
1.1.1.1,10.0.0.53) op het default bridge network of bij specifieke configuraties.
Docker’s embedded DNS doet twee dingen:
- Service discovery binnen het Docker-netwerk (containernaam → IP).
- Forwarding van externe queries naar upstream resolvers (meestal die van de host).
2) Waarom kan embedded DNS falen terwijl “internet” werkt?
Omdat je in feite twee lagen hebt:
- Container →
127.0.0.11(lokale stub in Docker) - Docker stub → upstream resolvers (host resolvers)
Problemen kunnen in elk segment zitten: UDP geblokkeerd, NAT kapot, upstream onbereikbaar, resolv.conf verkeerd, of search domains die queries “vervormen”.
Symptomen classificeren
Voor je commando’s gaat draaien: classificeer het symptoom. Dat bepaalt je volgende stap.
A) “Temporary failure in name resolution”
Typisch: geen antwoord van resolver, timeouts, of DNS-server onbereikbaar.
B) “Name or service not known” / NXDOMAIN
DNS antwoordt wél, maar met “bestaat niet”. Kan komen door search domains, split DNS, of verkeerde upstream.
C) Intermitterend: soms wel, soms niet
Denk aan: race conditions bij opstart, resolvers roteren, UDP-fragmentatie, conntrack issues, of meerdere upstreams waarvan één kapot is.
D) Alleen bepaalde domeinen falen
Denk aan: split-horizon (intern vs extern), VPN, corporate DNS, DNSSEC/EDNS, of blokkades.
E) Alleen in één image (Alpine vs Debian)
Musl (Alpine) gedraagt zich anders dan glibc (Debian/Ubuntu). Ook tooling verschilt.
Snelle triage: 60 seconden checklist
Voer dit uit op de host, dan in de container.
Op de host
docker info --format '{{json .}}' | jq '.ServerVersion, .SecurityOptions, .Runtimes'
ip addr show
ip route show
cat /etc/resolv.conf
Als je systemd-resolved gebruikt, zie je vaak nameserver 127.0.0.53 op de host. Dat is een lokale stub; Docker kan daar soms slecht mee overweg (afhankelijk van distro/versie/config).
Check resolutie op host:
getent hosts example.com
resolvectl query example.com 2>/dev/null || true
dig +short example.com @1.1.1.1
In een tijdelijke debug-container
Gebruik een image met tools:
docker run --rm -it --name dnsdebug --network bridge nicolaka/netshoot bash
Dan:
cat /etc/resolv.conf
getent hosts example.com
dig example.com
nslookup example.com
Als dig wél werkt maar je app niet: kijk naar nsswitch.conf, libc, of app-level DNS.
Inspecteer de container: resolv.conf, nsswitch, libc
1) /etc/resolv.conf en belangrijke opties
In container:
cat /etc/resolv.conf
Let op:
nameserver 127.0.0.11(Docker stub)search ...(kan queries veranderen)options ndots:5(bekend uit Kubernetes; kan ook in Docker voorkomen via resolv.conf van host)timeout:,attempts:
Waarom is ndots belangrijk?
Bij ndots:5 wordt een naam met minder dan 5 punten eerst als “relatief” gezien en gecombineerd met search domains. Een query naar api kan dan eerst api.corp.local, api.prod.local, etc proberen. Dat kan timeouts veroorzaken voordat de “echte” query gebeurt.
Test dit:
time getent hosts api
time getent hosts api.example.com
2) nsswitch.conf
cat /etc/nsswitch.conf | sed -n '1,120p'
Zoek naar:
hosts: files dns(normaal)hosts: files mdns4_minimal [NOTFOUND=return] dns(kan.localbeïnvloeden)
3) Welke libc?
Alpine gebruikt musl:
ldd --version 2>&1 | head -n 2
Op Debian/Ubuntu meestal glibc. Musl heeft andere resolutie-eigenschappen (o.a. minder features rond resolv.conf opties).
Docker embedded DNS (127.0.0.11) en user-defined networks
1) Begrijp je netwerkdriver
Toon netwerken:
docker network ls
docker network inspect bridge | jq '.[0].Options, .[0].IPAM'
Maak een user-defined network (aanrader voor service discovery):
docker network create --driver bridge appnet
docker run -d --name web --network appnet nginx:alpine
docker run --rm -it --network appnet nicolaka/netshoot sh
In die netshoot-container:
cat /etc/resolv.conf
getent hosts web
dig web
Verwacht: web resolveert naar het container-IP binnen appnet.
2) Embedded DNS forwarders en upstream resolvers
Docker haalt upstream resolvers vaak uit de host-resolv.conf. Maar als de host 127.0.0.53 gebruikt (systemd-resolved stub), kan Docker in sommige setups die stub niet correct bereiken vanuit de Docker daemon context.
Controleer Docker daemon config:
cat /etc/docker/daemon.json 2>/dev/null || echo "geen daemon.json"
Als je expliciet DNS wilt instellen voor alle containers:
{
"dns": ["1.1.1.1", "8.8.8.8"]
}
Na wijziging:
sudo systemctl restart docker
Let op: dit is een globale wijziging met impact op alle containers.
3) Per container DNS instellen
docker run --rm -it --dns 1.1.1.1 --dns 8.8.8.8 alpine:3.20 sh
cat /etc/resolv.conf
nslookup example.com
Als dit werkt maar zonder --dns niet: probleem zit in embedded DNS of in upstream resolvers die Docker gebruikt.
Host vs container: vergelijk resolutiepad
Doel: vaststellen of het probleem in de container zit, of in de host/daemon.
1) Test dezelfde resolver direct
Als container 127.0.0.11 gebruikt, test je upstream door direct naar een publieke resolver te vragen:
docker run --rm -it nicolaka/netshoot bash -lc \
'cat /etc/resolv.conf; dig +time=2 +tries=1 example.com @1.1.1.1; dig +time=2 +tries=1 example.com @8.8.8.8'
- Werkt direct naar
1.1.1.1maar niet via default? Dan is Docker stub/upstream config verdacht. - Werkt ook niet direct? Dan is egress/UDP/route/iptables/MTU verdacht.
2) Test TCP fallback
Sommige netwerken blokkeren UDP/53 of fragmenten. Test DNS over TCP:
docker run --rm -it nicolaka/netshoot bash -lc \
'dig +tcp example.com @1.1.1.1'
Als TCP werkt maar UDP niet: denk aan firewall/MTU/fragmentatie.
Packet capture: tcpdump op host én in container
Packet capture is de snelste manier om “waar stopt het” te beantwoorden.
1) Vind de veth-interface van je container
Pak container PID:
CID=$(docker ps -qf name=dnsdebug)
PID=$(docker inspect -f '{{.State.Pid}}' "$CID")
echo "$PID"
Bekijk netwerk namespace links:
sudo nsenter -t "$PID" -n ip addr
sudo nsenter -t "$PID" -n ip route
2) Capture in de container namespace
sudo nsenter -t "$PID" -n tcpdump -ni any port 53
Start in een andere terminal een query vanuit de container:
docker exec -it dnsdebug dig example.com
Interpretatie:
- Zie je een UDP-pakket naar
127.0.0.11:53? Dan gebruikt de container embedded DNS. - Zie je antwoord terug? Zo niet: embedded DNS niet bereikbaar of niet luisterend (zeldzaam), of container kan loopback niet gebruiken (zeer zeldzaam).
3) Capture op de host op docker0 of bridge
Check interfaces:
ip link show | grep -E 'docker0|br-'
Capture:
sudo tcpdump -ni docker0 port 53
Voor user-defined networks is het vaak br-<id>:
sudo tcpdump -ni br-$(docker network inspect appnet -f '{{.Id}}' | cut -c1-12) port 53
Je wilt zien: container → Docker stub → upstream resolver. Als je wel container → stub ziet, maar geen stub → upstream, dan zit het probleem in Docker daemon of host firewall.
iptables/nftables en forwarding/NAT
DNS is klein verkeer, maar afhankelijk van NAT/conntrack. Docker configureert regels automatisch. Als iemand iptables flush’t of nftables policy’s aanpast, krijg je “DNS kapot” als symptoom.
1) Check of je nftables of iptables gebruikt
sudo iptables -V
sudo nft --version
2) Toon Docker gerelateerde regels
iptables (legacy of nft-compat):
sudo iptables -S | sed -n '1,200p'
sudo iptables -t nat -S | sed -n '1,200p'
sudo iptables -L -n -v
sudo iptables -t nat -L -n -v
Zoek naar chains als:
DOCKERDOCKER-USERDOCKER-ISOLATION-STAGE-1
Veelvoorkomende fout: een te strikte DOCKER-USER chain die UDP/53 dropt.
Voorbeeld check:
sudo iptables -L DOCKER-USER -n -v --line-numbers
Als je daar een DROP ziet zonder uitzondering voor DNS/egress, fix dan beleid (voorbeeld: allow established/related en allow egress):
sudo iptables -I DOCKER-USER 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -I DOCKER-USER 2 -i docker0 -o eth0 -j ACCEPT
sudo iptables -A DOCKER-USER -j RETURN
Pas interfaces aan je situatie aan (eth0 kan ens3, wlan0, etc zijn).
3) IP forwarding
sysctl net.ipv4.ip_forward
sysctl net.ipv6.conf.all.forwarding
Voor IPv4 moet net.ipv4.ip_forward = 1 zijn voor routing/NAT.
MTU, fragmentatie en EDNS0
DNS-antwoorden kunnen groter zijn dan 512 bytes (EDNS0). Als je pad MTU-problemen heeft (VPN, overlay, PPPoE), kunnen gefragmenteerde UDP-pakketten verdwijnen. Resultaat: timeouts, vooral bij DNSSEC, TXT-records, of grote antwoorden.
1) Test met een “groot” antwoord
Bijvoorbeeld DNSKEY (vaak groot):
docker run --rm -it nicolaka/netshoot bash -lc \
'dig +dnssec org DNSKEY @1.1.1.1'
Als dit timeouts geeft maar dig example.com werkt: fragmentatie/MTU is verdacht.
2) Forceer kleinere UDP payload
docker run --rm -it nicolaka/netshoot bash -lc \
'dig +bufsize=512 org DNSKEY @1.1.1.1'
Als +bufsize=512 werkt maar default niet: je verliest fragmenten.
3) Check MTU in container en host
ip link show docker0
docker exec -it dnsdebug ip link
Als je via VPN een lagere MTU nodig hebt, kun je Docker bridge MTU aanpassen (globaal) via daemon config:
/etc/docker/daemon.json:
{
"mtu": 1400
}
Herstart Docker:
sudo systemctl restart docker
Trade-off: lagere MTU kan throughput beïnvloeden maar verhoogt betrouwbaarheid over tunnels.
IPv6, Happy Eyeballs en AAAA-problemen
Soms werkt A-record resolutie, maar AAAA (IPv6) timeouts geven vertraging. Applicaties proberen IPv6 eerst, of parallel (Happy Eyeballs). Als IPv6 routing/firewall half werkt, voelt het als “DNS traag/kapot”.
1) Test A en AAAA apart
docker run --rm -it nicolaka/netshoot bash -lc \
'dig +short A example.com; dig +short AAAA example.com'
2) Check IPv6 connectivity
docker run --rm -it nicolaka/netshoot bash -lc \
'ip -6 addr; ip -6 route; ping -6 -c 1 2606:4700:4700::1111 || true'
Als DNS AAAA teruggeeft maar IPv6 verkeer faalt, kan je app timeouts ervaren. Oplossingen:
- IPv6 correct configureren (beste).
- Of IPv6 uitschakelen in container/host (symptoombestrijding).
Per container kun je IPv6 niet triviaal “uit” zetten zonder sysctls/capabilities. Op host kun je:
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
Maar dit is ingrijpend; liever gericht oplossen.
VPN’s, systemd-resolved, NetworkManager en split DNS
1) systemd-resolved stub (127.0.0.53)
Op veel distro’s is /etc/resolv.conf een symlink naar systemd’s stub. Docker kan upstream resolvers uit /etc/resolv.conf lezen en dan 127.0.0.53 krijgen. Maar 127.0.0.53 is host-loopback, niet per se bereikbaar/zinvol voor Docker daemon in alle contexten.
Check:
ls -l /etc/resolv.conf
resolvectl status | sed -n '1,200p'
Als je split DNS hebt (VPN domeinen via VPN DNS, rest via public), dan is resolvectl leidend. Docker ziet dat niet altijd correct.
Oplossingsrichting: laat Docker naar de “echte” upstream servers wijzen (niet 127.0.0.53). Bijvoorbeeld door /etc/resolv.conf te laten wijzen naar /run/systemd/resolve/resolv.conf (met echte servers), of door Docker daemon "dns": [...] te zetten.
Op sommige systemen:
cat /run/systemd/resolve/resolv.conf
Als dat de echte DNS-servers bevat, kun je /etc/resolv.conf daarop laten wijzen (distro-afhankelijk, wees voorzichtig):
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
Impact: dit verandert host DNS gedrag; test goed.
2) VPN split DNS: bepaalde zones alleen intern
Symptoom: dig internal.corp werkt op host (via VPN), maar faalt in container.
Test in container:
docker run --rm -it nicolaka/netshoot bash -lc \
'dig internal.corp; dig internal.corp @10.0.0.53'
Als direct naar de corporate DNS (@10.0.0.53) werkt, maar via default niet: upstream selectie is fout. Je kunt per container --dns zetten naar de corporate resolver, of een lokale caching resolver op host draaien die split DNS correct doet en die aan Docker geven.
Kubernetes-achtige patronen in “gewone” Docker setups
Soms zie je options ndots:5 en lange search lijsten doordat images/entrypoints resolv.conf kopiëren of omdat je tooling uit K8s gewend bent. In Docker kan dat contraproductief zijn.
Diagnose: extreem trage resolutie voor korte namen.
Meet:
docker run --rm -it nicolaka/netshoot bash -lc \
'cat /etc/resolv.conf; time getent hosts foo; time getent hosts foo.example.com'
Oplossing: minimaliseer search domains en ndots, of gebruik FQDN’s in configs.
Je kunt resolv.conf opties per container beïnvloeden met --dns-search en --dns-opt:
docker run --rm -it \
--dns 1.1.1.1 \
--dns-opt ndots:1 \
--dns-search . \
alpine:3.20 sh -lc 'cat /etc/resolv.conf; nslookup example.com'
Let op: --dns-search . kan search uitschakelen (afhankelijk van libc/implementatie). Test altijd.
Oplossingsstrategieën (met trade-offs)
Hieronder een set oplossingen, van “minst invasief” naar “meest structureel”.
1) Voeg debug-tools toe (tijdelijk) en meet exact
Gebruik netshoot of installeer tools in je image (alleen voor debug):
Debian/Ubuntu:
apt-get update && apt-get install -y dnsutils iproute2 tcpdump iputils-ping
Alpine:
apk add --no-cache bind-tools iproute2 tcpdump iputils
2) Per container expliciete DNS-servers
Als je snel wilt isoleren:
docker run --rm -it --dns 1.1.1.1 --dns 8.8.8.8 yourimage
Trade-off: je omzeilt corporate/split DNS. Interne namen kunnen breken.
3) Docker daemon DNS globaal instellen
/etc/docker/daemon.json:
{
"dns": ["10.0.0.53", "1.1.1.1"]
}
Trade-off: alle containers gebruiken dit; kan gewenst zijn, maar let op VPN scenario’s (DNS server kan onbereikbaar zijn zonder VPN).
4) Lokale caching resolver op de host (aanrader bij complexiteit)
Run bijvoorbeeld unbound of dnsmasq op de host, laat die split DNS doen, en configureer Docker om die host-IP als resolver te gebruiken.
Belangrijk: containers kunnen niet naar 127.0.0.1 van de host. Gebruik het host-IP op de docker bridge (vaak 172.17.0.1) of een dedicated interface.
Voorbeeld: host luistert op 172.17.0.1:53, Docker daemon "dns": ["172.17.0.1"].
Trade-off: extra component, maar wel controleerbaar en observeerbaar.
5) MTU fixen
Als fragmentatie de boosdoener is: stel Docker MTU lager in.
Trade-off: performance.
6) Firewall beleid corrigeren (DOCKER-USER)
Zorg dat egress DNS toegestaan is. Maak het expliciet:
sudo iptables -I DOCKER-USER 1 -p udp --dport 53 -j ACCEPT
sudo iptables -I DOCKER-USER 2 -p tcp --dport 53 -j ACCEPT
sudo iptables -A DOCKER-USER -j RETURN
Trade-off: security posture; maak het zo beperkt mogelijk (bijv. alleen naar specifieke resolvers).
Reproduceerbare testmatrix en afsluitende tips
1) Testmatrix: wat je altijd wilt meten
Voer in een debug-container de volgende matrix uit en noteer wat faalt:
- Resolutie via default resolver:
dig +time=2 +tries=1 example.com - Resolutie direct naar upstream (1.1.1.1):
dig +time=2 +tries=1 example.com @1.1.1.1 - TCP DNS:
dig +tcp +time=2 +tries=1 example.com @1.1.1.1 - Groot antwoord (DNSSEC):
dig +dnssec org DNSKEY @1.1.1.1 - Alleen A/AAAA:
dig +short A example.com dig +short AAAA example.com - libc pad (getent):
getent hosts example.com
Interpretatie (snelle mapping):
- (1) faalt, (2) werkt → Docker embedded DNS / upstream config.
- (2) faalt, (3) werkt → UDP issues (firewall/fragmentatie).
- (4) faalt, (2) werkt → MTU/fragmentatie/EDNS0 issues.
- (6) faalt, (1) werkt → nsswitch/libc/app gedrag.
2) Debuggen in productie zonder image-wijzigingen
Gebruik docker exec en nsenter:
docker exec -it <container> sh
Als container geen shell/tools heeft, start een “sidecar” debug-container in hetzelfde netwerk:
docker run --rm -it --network container:<container> nicolaka/netshoot bash
Dan debug je alsof je in de target container zit qua netwerk namespace.
3) Let op caching
- Applicaties (Java, Go, nginx) kunnen DNS cachen.
- systemd-resolved cached.
- Docker embedded DNS kan ook gedrag vertonen dat “sticky” lijkt.
Voor Java: check TTL settings (networkaddress.cache.ttl). Voor Go: afhankelijk van libc vs pure Go resolver.
4) Documenteer je uiteindelijke “known good” configuratie
Leg vast:
- Welke DNS servers containers moeten gebruiken (public vs corporate).
- Of je split DNS nodig hebt.
- MTU waarden.
- Firewall policies (DOCKER-USER chain).
- Of IPv6 ondersteund is.
Conclusie
Gevorderd DNS-debuggen in Docker is vooral: laag voor laag bewijzen waar het verkeer stopt en welke resolverketen gebruikt wordt. Begin bij /etc/resolv.conf en 127.0.0.11, vergelijk met directe queries naar upstream resolvers, en ga dan naar packet capture en firewall/NAT. Als je deze workflow volgt, kun je vrijwel elk “Temporary failure in name resolution” probleem terugbrengen tot een concrete oorzaak: upstream mismatch (systemd-resolved/VPN), UDP-fragmentatie/MTU, firewall policies, of libc/app-specifiek gedrag.
Als je wilt, kun je je docker network inspect, /etc/resolv.conf (host + container), en een paar dig outputs delen; dan kan ik een gerichte diagnoseboom opstellen voor jouw specifieke situatie.