Le DNS à l’intérieur des conteneurs échoue de la manière la plus démoralisante : pas tout le temps. Vous livrez une image, les contrôles de santé sont verts, puis l’application ne peut pas résoudre db.internal pendant deux minutes, se rétablit, échoue à nouveau, et votre chronologie d’incident se transforme en performance improvisée.
Sur les Linux modernes, ce n’est souvent pas tant le “DNS Docker” que “le /etc/resolv.conf de l’hôte pointe vers le résolveur stub de systemd (127.0.0.53), et Docker l’a copié dans un namespace réseau où cette adresse ne signifie pas ce que vous pensez.” Le corriger une fois est facile. Le rendre durable à travers reboots, changements de VPN, renouvellements DHCP et mises à jour de distribution est le vrai travail.
Plan de diagnostic rapide
Voici l’ordre que j’utilise quand quelqu’un dit « Le DNS est cassé dans Docker ». Il est conçu pour identifier rapidement le goulot d’étranglement, pas pour assouvir une curiosité philosophique.
Première étape : est-ce spécifique au conteneur ou du côté hôte ?
- Résolvez depuis l’hôte le même nom que celui qui échoue dans le conteneur.
- Résolvez depuis un conteneur frais attaché au même réseau que le service en panne.
- Comparez les serveurs DNS dans le
/etc/resolv.confde l’hôte et celui du conteneur.
Si l’hôte ne peut pas non plus résoudre, ce n’est pas un problème Docker. C’est un problème DNS que Docker hérite poliment.
Deuxième étape : 127.0.0.53 est-il impliqué ?
Si le /etc/resolv.conf du conteneur liste nameserver 127.0.0.53, c’est souvent l’indice fatal sur les systèmes avec systemd-resolved. Dans le namespace réseau du conteneur, 127.0.0.53 renvoie au conteneur lui-même, pas au stub de l’hôte.
Troisième étape : le DNS embarqué de Docker (127.0.0.11) échoue-t-il, ou est-ce en amont ?
Sur les réseaux bridge définis par l’utilisateur, Docker fournit un serveur DNS embarqué à 127.0.0.11 qui relaie les requêtes en amont. Si les conteneurs ne peuvent pas résoudre les noms des services (entre conteneurs) et ne peuvent pas résoudre les noms externes, le DNS embarqué de Docker ou le chemin iptables peut être cassé. Si la découverte de services fonctionne mais pas l’externe, les résolveurs en amont sont probablement incorrects.
Quatrième étape : avez-vous affaire à du split DNS ?
Les VPN et réseaux d’entreprise adorent le split DNS : les domaines internes vont vers des résolveurs internes ; tout le reste va en public. systemd-resolved gère cela élégamment. Docker… beaucoup moins. Attendez-vous à ce qu’un DNS “corrigé” échoue encore pour *.corp à moins d’enseigner explicitement à Docker quels sont les résolveurs internes.
Cinquième étape : vérifiez MTU / fallback TCP / bizarreries EDNS
Les timeouts intermittents, surtout via VPN, peuvent venir de paquets DNS fragmentés ou perdus. Forcez TCP comme test. Si TCP fonctionne et UDP pas, vous êtes en zone PMTU / pare-feu.
Ce qui casse réellement : Docker, resolv.conf et systemd-resolved
Nommez les pièces en mouvement :
- systemd-resolved est un gestionnaire local de résolution. Il peut exécuter un “écouteur stub” sur
127.0.0.53, maintenir des serveurs DNS par lien et prendre en charge le routage split DNS par domaine. - /etc/resolv.conf est le fichier que les resolvers libc lisent pour trouver les serveurs de noms et les domaines de recherche. Il peut être un vrai fichier ou un lien symbolique géré par systemd-resolved.
- Docker génère le
/etc/resolv.confd’un conteneur en se basant sur la configuration du résolveur de l’hôte, avec des différences de comportement selon le mode réseau et si vous overridez les paramètres DNS. - DNS embarqué de Docker (
127.0.0.11) existe sur les réseaux bridge définis par l’utilisateur. Il fournit la résolution des noms de conteneurs et relaie les requêtes vers des serveurs en amont.
Le mode d’échec classique se déroule ainsi :
- Votre distribution active systemd-resolved.
/etc/resolv.confdevient un lien symbolique vers un stub qui pointe versnameserver 127.0.0.53.- Docker voit cela et écrit le même nameserver dans les conteneurs.
- À l’intérieur des conteneurs,
127.0.0.53n’est pas le résolveur de l’hôte. C’est le loopback du conteneur. - Les requêtes expirent ou échouent avec “connection refused”. Les applications interprètent cela comme “le DNS est en panne”. Elles ont raison.
Il existe un deuxième mode d’échec, plus subtil : Docker utilise 127.0.0.11 dans les conteneurs, qui relaie vers des résolveurs en amont. Mais l’amont est peuplé depuis la config de l’hôte au démarrage de Docker. Puis votre VPN se connecte, systemd-resolved change les serveurs par lien, et Docker continue de relayer vers les anciens. Tout semble bien jusqu’à ce que vous ayez besoin d’un domaine interne. Alors c’est “pourquoi mon portable le résout et pas le conteneur ?”
Et oui, vous pouvez masquer le problème en codant en dur 8.8.8.8 dans Docker. C’est l’équivalent ingénierie de traiter une douleur thoracique avec une boisson énergisante. Ça peut rendre le tableau de bord vert. Ça peut aussi contourner le DNS interne, violer la politique, casser le split DNS et rendre le débogage en production misérable.
Une idée paraphrasée de Dan Kaminsky (chercheur DNS) qui a bien vieilli : Le DNS est trompeusement simple jusqu’à ce qu’il échoue, et ensuite il échoue d’une manière qui ressemble à tout le reste.
(idée paraphrasée)
Une blague, puisque nous y sommes : le DNS est la dépendance qui peut échouer et vous convaincre quand même que votre application est le problème. C’est comme être ghosté par une adresse IP.
Faits intéressants et historique (ce qui explique les bizarreries)
- Fait 1 : L’écouteur stub de systemd-resolved utilise
127.0.0.53par convention, pas par magie. Ce n’est que le loopback, et les namespaces changent ce que loopback signifie. - Fait 2 : Sur de nombreuses versions d’Ubuntu,
/etc/resolv.confest un lien vers/run/systemd/resolve/stub-resolv.confquand systemd-resolved est activé. - Fait 3 : systemd-resolved maintient deux variantes générées de resolv.conf : un fichier stub (pointant vers
127.0.0.53) et un fichier “amont réel” (listant les serveurs DNS réels) généralement à/run/systemd/resolve/resolv.conf. - Fait 4 : Le DNS embarqué de Docker à
127.0.0.11n’est pas un résolveur récursif général. C’est un forwarder plus découverte de services conteneurs, et il dépend d’un amont DNS correct. - Fait 5 : L’option
ndotsdans resolv.conf change la fréquence d’ajout des “domaines de recherche”. Unndotsélevé combiné à de longues listes de recherche peut transformer une seule requête en plusieurs requêtes DNS et donner l’impression de lenteur. - Fait 6 : Le resolver glibc a historiquement privilégié UDP et revient sur TCP. Certains pare-feu laissent passer les réponses UDP fragmentées, provoquant des retards de fallback et des timeouts qui paraissent intermittents.
- Fait 7 : Avant que le DNS embarqué de Docker ne devienne courant sur les réseaux définis par l’utilisateur, le comportement DNS des conteneurs variait plus et les gens attachaient fréquemment les conteneurs au réseau hôte pour « réparer le DNS », créant généralement de nouveaux problèmes.
- Fait 8 : Le split DNS (routage par domaine vers des résolveurs spécifiques) est courant en entreprise et est une fonctionnalité de premier ordre de systemd-resolved ; Docker ne réplique pas intrinsèquement le routage par domaine sauf si vous le configurez délibérément.
- Fait 9 : Un nombre surprenant de « pannes DNS » dans des flottes de conteneurs concernent en réalité le comportement de cache : le résolveur du conteneur met en cache différemment que l’hôte, ou l’application met en cache de mauvais résultats et ne retente jamais correctement.
Tâches pratiques : commandes, sorties attendues et décisions
On ne corrige pas le DNS à la bonne volonté. On le corrige avec des observations ciblées et des décisions. Voici les tâches pratiques que j’exécute réellement, avec ce que la sortie signifie et quoi faire ensuite.
Tâche 1 : Vérifier ce que l’hôte pense de /etc/resolv.conf
cr0x@server:~$ ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 39 Nov 18 09:02 /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf
Signification : Le fichier de résolveur de l’hôte est un lien symbolique vers le stub. Attendez-vous à trouver 127.0.0.53 dedans.
Décision : Vous devrez probablement faire en sorte que Docker utilise le fichier des résolveurs en amont à la place, ou définir explicitement des serveurs DNS pour Docker.
Tâche 2 : Inspecter le stub resolv.conf de l’hôte
cr0x@server:~$ cat /run/systemd/resolve/stub-resolv.conf
# This file is managed by man:systemd-resolved(8). Do not edit.
nameserver 127.0.0.53
options edns0 trust-ad
search corp.example
Signification : L’hôte utilise le stub local de systemd. Parfait sur l’hôte, toxique si copié dans les conteneurs.
Décision : Ne pointez pas les conteneurs vers 127.0.0.53. Assurez-vous qu’ils utilisent de vrais serveurs DNS en amont.
Tâche 3 : Inspecter la liste des résolveurs “réels” de l’hôte
cr0x@server:~$ cat /run/systemd/resolve/resolv.conf
# This file is managed by man:systemd-resolved(8). Do not edit.
nameserver 10.20.30.40
nameserver 10.20.30.41
search corp.example
Signification : Ce sont les serveurs DNS en amont avec lesquels systemd-resolved communiquera.
Décision : Ce sont généralement les bons serveurs à fournir à Docker, soit via la configuration du démon Docker soit en réorientant /etc/resolv.conf loin du stub (avec prudence).
Tâche 4 : Confirmer le statut de systemd-resolved et l’écouteur stub
cr0x@server:~$ systemctl status systemd-resolved --no-pager
● systemd-resolved.service - Network Name Resolution
Loaded: loaded (/lib/systemd/system/systemd-resolved.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2026-01-02 07:41:12 UTC; 2h 18min ago
...
DNS Stub Listener: yes
Signification : Resolved est actif et l’écouteur stub est activé.
Décision : Si vous désactivez l’écouteur stub, vous devez vous assurer que le système possède toujours un resolv.conf utilisable. Désactiver aveuglément est une bonne façon de créer une panne côté hôte.
Tâche 5 : Voir les DNS par lien, y compris le routage split DNS
cr0x@server:~$ resolvectl status
Global
LLMNR setting: yes
MulticastDNS setting: no
DNSOverTLS setting: no
DNSSEC setting: no
DNSSEC supported: no
Current DNS Server: 10.20.30.40
DNS Servers: 10.20.30.40 10.20.30.41
DNS Domain: corp.example
Link 2 (ens18)
Current Scopes: DNS
Protocols: +DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 10.20.30.40
DNS Servers: 10.20.30.40 10.20.30.41
DNS Domain: corp.example
Signification : Confirme quels serveurs et domaines DNS sont actifs. Si un lien VPN existe, il peut avoir ses propres domaines et serveurs.
Décision : Si vous dépendez du split DNS, vous ne pouvez pas juste “utiliser 1.1.1.1” et considérer le problème réglé. Vous devez propager les résolveurs internes dans Docker.
Tâche 6 : Vérifier ce que Docker pense des DNS (vue du démon)
cr0x@server:~$ docker info --format '{{json .}}' | jq '.DockerRootDir, .Name, .SecurityOptions'
"/var/lib/docker"
"server"
[
"name=apparmor",
"name=seccomp,profile=default"
]
Signification : Cela n’affiche pas directement le DNS, mais confirme que vous n’êtes pas dans un chemin d’exécution exotique. Nous posons le contexte.
Décision : Procédez à l’inspection du resolv.conf d’un conteneur et de la configuration du démon.
Tâche 7 : Inspecter la config DNS à l’intérieur d’un conteneur en cours d’exécution
cr0x@server:~$ docker exec -it web01 cat /etc/resolv.conf
nameserver 127.0.0.53
options edns0 trust-ad
search corp.example
Signification : Ce conteneur pointe vers lui-même pour le DNS. Il échouera sauf si quelque chose dans le conteneur écoute sur 127.0.0.53:53 (ce qui n’est pas le cas).
Décision : Corrigez l’entrée DNS fournie à Docker (configuration du démon) ou la liaison de /etc/resolv.conf de l’hôte afin que les conteneurs obtiennent de vrais serveurs en amont.
Tâche 8 : Test fonctionnel rapide depuis l’intérieur d’un conteneur
cr0x@server:~$ docker exec -it web01 getent ahosts example.com
getent: Name or service not known
Signification : La résolution libc échoue. Ce n’est pas “curl ne peut pas atteindre internet”, c’est l’échec de la résolution de noms.
Décision : Confirmez l’accessibilité du nameserver et si le DNS embarqué de Docker est en jeu.
Tâche 9 : Déterminer si le DNS embarqué de Docker est utilisé (127.0.0.11)
cr0x@server:~$ docker run --rm alpine:3.19 cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0
Signification : Ce conteneur est sur un réseau défini par l’utilisateur ou Docker a décidé d’utiliser le DNS embarqué. Bien : cela évite le piège 127.0.0.53. Mauvais : il a toujours besoin de serveurs en amont corrects.
Décision : Si la résolution externe échoue avec 127.0.0.11, enquêtez sur les DNS amont du démon Docker, iptables ou le pare-feu.
Tâche 10 : Tester la reachabilité du DNS amont depuis le namespace réseau de l’hôte
cr0x@server:~$ dig +time=1 +tries=1 @10.20.30.40 example.com A
; <<>> DiG 9.18.24 <<>> +time=1 +tries=1 @10.20.30.40 example.com A
;; NOERROR, id: 22031
;; ANSWER SECTION:
example.com. 300 IN A 93.184.216.34
Signification : Le résolveur amont est joignable et répond.
Décision : Si les conteneurs échouent encore, le problème est probablement le forwarding conteneur→hôte, le relai DNS de Docker, ou le NAT/iptables.
Tâche 11 : Tester la reachabilité vers le résolveur amont depuis l’intérieur d’un conteneur
cr0x@server:~$ docker run --rm alpine:3.19 sh -c "apk add --no-cache bind-tools >/dev/null; dig +time=1 +tries=1 @10.20.30.40 example.com A"
; <<>> DiG 9.18.24 <<>> +time=1 +tries=1 @10.20.30.40 example.com A
;; connection timed out; no servers could be reached
Signification : Depuis le réseau du conteneur, ce résolveur n’est pas joignable. Cela peut être le routage, le pare-feu, ou l’accès limité au VPN depuis le namespace hôte uniquement.
Décision : Si les résolveurs internes ne sont accessibles que via des routes hôtes non NATées pour les conteneurs, vous devez corriger le routage/NAT, utiliser le réseau hôte pour ce service ou exécuter un forwarder DNS que les conteneurs peuvent atteindre.
Tâche 12 : Inspecter le réseau Docker et les détails DNS du conteneur
cr0x@server:~$ docker inspect web01 --format '{{json .HostConfig.Dns}} {{json .NetworkSettings.Networks}}' | jq .
[
null,
{
"appnet": {
"IPAMConfig": null,
"Links": null,
"Aliases": [
"web01",
"web"
],
"NetworkID": "b3c2f5cbd7c7f0d7d3b7d7aa3d2c51a9c7bd22b9f5a3db0a3d35a8a2c4d9a111",
"EndpointID": "c66d42e9a07b6d6a8e6d6d3fb5a5a0de27b8464a9e7d0a2c4e5b11aa3aa2beef",
"Gateway": "172.18.0.1",
"IPAddress": "172.18.0.10",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:12:00:0a",
"DriverOpts": null
}
}
]
Signification : Aucun DNS explicite défini sur le conteneur ; il a hérité des valeurs par défaut. Le réseau est un bridge défini par l’utilisateur appnet.
Décision : Si les valeurs par défaut sont mauvaises, corrigez au niveau du démon ou par réseau/service via Compose. Préférez le niveau démon pour la cohérence.
Tâche 13 : Vérifier le fichier de configuration du démon Docker
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"log-driver": "journald"
}
Signification : Aucune configuration DNS n’est définie. Docker prendra des indices depuis le résolveur de l’hôte et ce qu’il a capturé au démarrage.
Décision : Ajoutez des serveurs DNS explicites (et éventuellement des domaines/options de recherche) si votre hôte utilise le stub systemd-resolved ou si vos upstream changent fréquemment.
Tâche 14 : Valider si les conteneurs atteignent le port DNS embarqué de Docker
cr0x@server:~$ docker run --rm alpine:3.19 sh -c "apk add --no-cache drill >/dev/null; drill @127.0.0.11 example.com | head"
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 59030
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; example.com. IN A
Signification : Le DNS embarqué répond. Si la résolution échoue encore dans l’application, vous pouvez avoir des problèmes d’options libc/search, pas de connectivité DNS brute.
Décision : Inspectez ndots, les domaines de recherche et le cache applicatif.
Tâche 15 : Vérifier les tempêtes de requêtes induites par search/ndots
cr0x@server:~$ docker exec -it web01 sh -c "cat /etc/resolv.conf; echo; getent hosts api"
nameserver 127.0.0.11
options ndots:5
search corp.example svc.cluster.local
getent: Name or service not known
Signification : Avec ndots:5, le résolveur traite les noms courts comme “relatifs” et essaie d’abord les domaines de recherche. Cela peut causer des échecs lents si ces suffixes ne résolvent pas.
Décision : Pour des charges Docker non-Kubernetes, gardez ndots modéré (souvent 0–1) et taillez les domaines de recherche. Ou apprenez aux applications à utiliser des FQDN.
Tâche 16 : Capturer le trafic DNS pour prouver où il meurt
cr0x@server:~$ sudo tcpdump -ni any port 53 -c 10
tcpdump: data link type LINUX_SLL2
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
10:22:01.112233 vethabc123 Out IP 172.18.0.10.45122 > 10.20.30.40.53: 12345+ A? example.com. (28)
10:22:02.113244 vethabc123 Out IP 172.18.0.10.45122 > 10.20.30.40.53: 12345+ A? example.com. (28)
10:22:03.114255 vethabc123 Out IP 172.18.0.10.45122 > 10.20.30.40.53: 12345+ A? example.com. (28)
Signification : Les requêtes quittent le veth du conteneur, mais il n’y a pas de réponses. Ce n’est pas un problème libc. C’est un problème de chemin, de pare-feu ou l’amont qui vous ignore.
Décision : Vérifiez le routage vers le résolveur depuis le bridge Docker, confirmez le NAT/masquerade, vérifiez que l’amont accepte les requêtes de cette source.
Correctifs durables (choisissez une stratégie et tenez-vous-y)
Il existe plusieurs correctifs valides. Ce qui est invalide, c’est de les mélanger au hasard jusqu’à ce que “ça marche sur mon laptop”. Choisissez une stratégie qui correspond à votre environnement : laptops avec churn VPN, serveurs avec résolveurs statiques ou réseaux d’entreprise mixtes.
Stratégie A (la plus courante) : Configurer le démon Docker avec des serveurs DNS explicites
C’est l’approche brutale et efficace : dites à Docker quels serveurs DNS fournir aux conteneurs (ou utiliser comme amont pour le DNS embarqué). C’est stable face au stub systemd-resolved et ne dépend pas des jeux de liens symboliques de /etc/resolv.conf.
À faire quand : votre environnement a des IPs de résolveurs connues (résolveurs internes, ou un forwarder DNS local) et vous voulez un comportement prévisible.
cr0x@server:~$ sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
"dns": ["10.20.30.40", "10.20.30.41"],
"dns-search": ["corp.example"],
"dns-opts": ["timeout:2", "attempts:2"]
}
EOF
cr0x@server:~$ sudo systemctl restart docker
Ce qu’il faut vérifier : Les nouveaux conteneurs devraient afficher soit ces résolveurs directement, soit 127.0.0.11 avec un forward correct. Les conteneurs existants peuvent nécessiter un redémarrage pour prendre les nouveaux paramètres.
Compromis : Excellente cohérence. Moins bien si vos serveurs DNS sont dynamiques (comme fournis par DHCP sur des laptops). Pour les laptops, envisagez de pointer Docker vers un forwarder local qui suit systemd-resolved.
Stratégie B : Pointer /etc/resolv.conf de l’hôte vers le fichier “amont réel” de systemd-resolved
Ce correctif fait hériter à Docker les vrais serveurs en amont en changeant la cible de /etc/resolv.conf. C’est efficace et rapide, mais cela change aussi le comportement de l’hôte. Si vous ne comprenez pas les conséquences, ne le faites pas sur des nœuds de production sans plan de retour arrière.
À faire quand : vous voulez que Docker hérite automatiquement du DNS amont et vous acceptez de contourner l’écouteur stub pour les clients libc lisant /etc/resolv.conf.
cr0x@server:~$ sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
cr0x@server:~$ ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 32 Jan 2 10:31 /etc/resolv.conf -> /run/systemd/resolve/resolv.conf
Signification : Vous avez fait en sorte que /etc/resolv.conf liste les serveurs en amont réels, pas 127.0.0.53.
Décision : Redémarrez Docker pour qu’il relise la config de l’hôte ; puis redémarrez les conteneurs affectés.
cr0x@server:~$ sudo systemctl restart docker
Compromis : Le split DNS peut devenir moins élégant si vous comptiez sur systemd-resolved pour faire du routage par domaine pour les clients stub. Certaines apps parlent à resolved via des modules NSS ; beaucoup lisent simplement resolv.conf. Connaissez votre stack.
Stratégie C : Exécuter un forwarder DNS local joignable par les conteneurs et pointer Docker dessus
Si vous avez du split DNS, du churn VPN ou “l’hôte peut atteindre les résolveurs internes mais pas les conteneurs”, un forwarder local est la colle ennuyeuse et correcte. Le forwarder écoute sur une adresse accessible depuis les conteneurs (souvent la gateway du bridge Docker) et relaie vers systemd-resolved ou des résolveurs en amont.
À faire quand : les IPs des résolveurs changent souvent, ou les résolveurs internes ne sont accessibles que depuis le namespace de l’hôte.
Un schéma pratique : exécuter dnsmasq ou unbound sur l’hôte, écoutant sur 172.18.0.1 (la gateway de votre bridge Docker) et le configurer pour relayer vers les amonts de systemd-resolved. Puis configurer Docker daemon avec "dns": ["172.18.0.1"].
Pourquoi ça tient : Docker obtient une IP DNS stable. Le forwarder peut suivre les changements de systemd-resolved ou être rechargé sur les événements de changement de lien.
Stratégie D : Utiliser les overrides DNS par service dans Compose avec parcimonie
Compose permet de définir dns, dns_search, dns_opt par service. Utile pour des exceptions, mais ce n’est pas une stratégie à l’échelle d’une flotte. Vous oublierez le cas spécial dans six mois et le redécouvrirez pendant une panne.
À utiliser quand : un service a besoin d’un résolveur spécial pour une courte période, ou vous migrez.
Stratégie E : Mode réseau host pour les charges sensibles au DNS (dernier recours)
Oui, --network host fait disparaître beaucoup de problèmes DNS parce que le conteneur partage le namespace de l’hôte. Il supprime aussi l’isolation réseau et augmente le rayon d’explosion. C’est acceptable pour un conteneur de debug. C’est un dernier recours pour des services production sauf si vous avez de bonnes raisons.
Deuxième blague (et dernière) : utiliser le réseau host pour réparer le DNS, c’est comme réparer un robinet qui fuit en retirant toute la plomberie. Techniquement, il n’y a plus de fuite.
Split DNS, VPNs et réseaux d’entreprise
Le split DNS est la provenance de la plupart des tickets “ça marche sur l’hôte mais pas dans Docker”. systemd-resolved peut router les requêtes en fonction du suffixe de domaine vers des liens et serveurs spécifiques. Le modèle Docker est plus simple : les conteneurs reçoivent un resolv.conf avec une liste de serveurs de noms, plus éventuellement des domaines de recherche. Il n’existe pas de sémantique native “envoyer corp.example au résolveur A et tout le reste au résolveur B” dans resolv.conf au-delà de l’ordre des serveurs et de la liste de recherche.
Ce qui arrive en pratique :
- Votre hôte résout
git.corp.exampleparce que systemd-resolved sait que ce domaine appartient à l’interface VPN. - Votre conteneur utilise
10.20.30.40parce que c’était “global” au démarrage de Docker, mais il ne connaît pas le résolveur spécifique au VPN arrivé plus tard. - Vous avez codé en dur un résolveur public dans Docker, et maintenant les noms internes ne se résolvent plus du tout.
Quand vous êtes dans ce monde, la réponse “correcte” est généralement d’exécuter un forwarder DNS sur l’hôte capable de faire le routage split, et de faire interroger ce forwarder par les conteneurs. systemd-resolved peut être cette intelligence, mais les conteneurs ne peuvent pas interroger en toute sécurité le stub à 127.0.0.53. Donc vous devez soit :
- Exposer un listener resolved sur une adresse non-loopback (pas ma solution préférée), ou
- Utiliser un forwarder qui interroge resolved via son fichier amont ou via une intégration D-Bus, ou
- Pousser les mêmes résolveurs internes dans Docker et accepter que vous deviez redémarrer Docker quand le VPN change.
Pour les laptops, “redémarrer Docker quand le VPN se connecte” n’est pas élégant, mais c’est honnête. Automatisez-le avec un script dispatcher NetworkManager si nécessaire. Pour les serveurs, préférez des résolveurs stables ou un forwarder local.
Trois mini-histoires du monde entreprise
1) Incident causé par une mauvaise hypothèse : « 127.0.0.53 est toujours le résolveur de l’hôte »
Une entreprise SaaS de taille moyenne avait une flotte d’hôtes Ubuntu exécutant Docker. L’équipe plateforme a standardisé sur systemd-resolved parce qu’il gérait proprement les clients VPN et maintenait l’état des résolveurs. Quelqu’un a remarqué que /etc/resolv.conf pointait vers 127.0.0.53 et a conclu, en toute confiance, que tous les processus locaux — y compris les conteneurs — devaient simplement l’utiliser.
Ils ont construit une image de base avec un health check : exécuter getent hosts api.corp.example. Cela passait de façon intermittente en staging, ce qu’ils ont interprété comme “le DNS est instable”. Ils ont augmenté les timeouts. Ils ont ajouté des retries. Ils ont blâmé l’équipe DNS. Classique.
L’incident en production est arrivé un lundi matin : plusieurs services n’ont pas pu se connecter aux API internes. Le DNS externe fonctionnait parfois (merci le cache et certains services utilisant des IPs directes). Les noms internes échouaient de façon constante. Les premiers intervenants ont redémarré des conteneurs et ont vu certaines récupérations, ce qui était pire qu’une panne constante : cela suggérait que le problème était “dans l’app”.
La correction a pris 20 minutes une fois que quelqu’un a réellement regardé le /etc/resolv.conf d’un conteneur et a réalisé qu’il contenait 127.0.0.53. Dans le namespace du conteneur, c’est le loopback du conteneur. Rien n’écoutait sur le port 53. Ils ont mis à jour la configuration DNS du démon Docker pour utiliser les résolveurs en amont et ont déroulé des redémarrages. Le postmortem était douloureux mais utile : les hypothèses sur les adresses loopback ne survivent pas aux namespaces.
2) Une optimisation qui s’est retournée contre eux : « On va réduire la latence DNS en fixant des résolveurs publics »
Une grande équipe d’entreprise faisait tourner des postes dev avec des setups similaires à Docker Desktop sur Linux. Leurs serveurs DNS internes étaient parfois lents aux heures de pointe. Un ingénieur bien intentionné a proposé une “optimisation” : définir le DNS du démon Docker sur des résolveurs publics pour améliorer les performances de lookup pour les dépendances externes (dépôts de paquets, registres).
Ça a semblé fonctionner pendant une semaine. Les builds étaient un peu plus rapides. Les plaintes sur des résolutions lentes apt ont cessé. Puis un nouveau service interne a été lancé avec un nom existant uniquement dans le DNS interne. Les conteneurs ont commencé à ne plus résoudre ce nom alors que l’hôte le résolvait bien. Les échecs étaient confus : les devs pouvaient curl le service depuis l’hôte, mais pas depuis leurs conteneurs.
Le débogage s’est transformé en renvoi de balle entre équipes applicatives et infra. Finalement, quelqu’un a remarqué que le démon Docker avait été fixé sur des DNS publics, contournant complètement le split DNS. L’“optimisation” avait silencieusement supprimé l’accès à la résolution interne pour tous les conteneurs, et dans certains environnements, elle enfreignait aussi la politique autour de la journalisation DNS et du contrôle d’exfiltration des données.
Le rollback a été simple : remettre les DNS Docker sur les résolveurs internes et ajouter un forwarder local en cache pour réduire la latence sans contourner les contrôles. La leçon était pratique : optimiser le DNS en choisissant un résolveur “rapide” est un piège quand votre organisation utilise le DNS comme mécanisme de routage et de politique.
3) Pratique ennuyeuse mais correcte qui a sauvé la mise : « On standardise les tests DNS en CI et sur les hôtes »
Une entreprise de paiements avait été brûlée par des pannes DNS intermittentes lors d’une migration antérieure. Ils ont répondu avec un playbook ennuyeux : chaque nœud avait une petite image de diagnostic disponible localement, et chaque pipeline de déploiement exécutait une suite de sanity DNS avant et après le déploiement.
La suite n’était pas sophistiquée. Elle vérifiait que les conteneurs pouvaient résoudre un nom public, un nom interne et un nom de service sur le réseau Docker. Elle vérifiait aussi la présence de 127.0.0.53 dans le resolv.conf du conteneur, parce qu’ils avaient déjà vu ce film. Le pipeline échouait vite si l’une de ces vérifications échouait.
Six mois plus tard, une mise à jour de distribution a changé la gestion de /etc/resolv.conf sur une nouvelle image de base pour les nœuds. Sur la moitié des nouveaux nœuds, Docker a commencé à fournir 127.0.0.53 dans les conteneurs. Les tests l’ont détecté avant un large déploiement. L’incident est resté anecdotique : un petit lot de nœuds n’a jamais été mis en service jusqu’à correction.
Ce n’est pas de l’ingénierie glamour. C’est celle qui vous permet de dormir. Des tests diagnostics standardisés n’empêchent pas toutes les pannes, mais ils stoppent les plus bêtes qui se répètent.
Erreurs courantes : symptôme → cause racine → correctif
1) « Les conteneurs ne peuvent rien résoudre ; l’hôte résout bien »
Symptôme : getent hosts example.com échoue dans le conteneur ; fonctionne sur l’hôte.
Cause racine : Le /etc/resolv.conf du conteneur contient nameserver 127.0.0.53 hérité du stub systemd-resolved.
Correctif : Configurez le démon Docker avec des serveurs en amont réels, ou repointez /etc/resolv.conf de l’hôte vers /run/systemd/resolve/resolv.conf et redémarrez Docker.
2) « Le DNS externe fonctionne, les domaines internes échouent (surtout après la connexion VPN) »
Symptôme : example.com se résout ; git.corp.example échoue dans les conteneurs.
Cause racine : Docker a capturé des serveurs DNS avant que le lien VPN ajoute le split DNS ; les conteneurs ne voient pas les résolveurs fournis par le VPN.
Correctif : Utilisez un forwarder DNS local et pointez Docker dessus, ou redémarrez Docker lors des changements d’état VPN (en acceptant la perturbation), ou définissez explicitement les serveurs DNS internes dans Docker.
3) « Timeouts DNS intermittents ; les retries aident »
Symptôme : Certaines requêtes expirent puis réussissent ; les logs montrent des rafales d’erreurs DNS.
Cause racine : Fragmentation UDP/MTU à travers le VPN ; réponses EDNS0 perdues ; pare-feu bloquant les gros UDP DNS.
Correctif : Testez avec des requêtes TCP ; ajustez le MTU ; envisagez de désactiver EDNS0 pour des chemins spécifiques ; ou exécutez un forwarder local qui gère le TCP en amont.
4) « La découverte de services à l’intérieur du réseau Docker échoue »
Symptôme : Le conteneur A ne peut pas résoudre le conteneur B par nom sur un réseau défini par l’utilisateur.
Cause racine : Chemin du DNS embarqué cassé, ou le conteneur est sur le bridge par défaut sans les sémantiques DNS embarquées, ou un --dns conflictuel désactive selon la configuration les attentes de découverte de service de Docker.
Correctif : Utilisez un réseau bridge défini par l’utilisateur ; évitez d’overrider le DNS par conteneur sauf si nécessaire ; vérifiez la santé du driver réseau Docker ; inspectez les règles iptables.
5) « Les lookups sont lents ; CPU en pic dans l’app pendant les appels DNS »
Symptôme : Latence augmentée ; threads d’app bloqués sur le DNS ; volume élevé de requêtes.
Cause racine : Liste de domaines de recherche trop grande + ndots élevé provoquant plusieurs requêtes par lookup ; l’app utilise des noms courts.
Correctif : Réduisez les domaines de recherche ; fixez ndots de façon appropriée ; utilisez des FQDN dans les configs ; envisagez un résolveur en cache local.
6) « On a défini le DNS Docker et maintenant certaines apps utilisent encore d’anciens résolveurs »
Symptôme : Après modification de daemon.json, certains conteneurs ont encore un ancien resolv.conf.
Cause racine : Les conteneurs existants conservent leur resolv.conf généré jusqu’à recréation/redémarrage (selon le runtime et les bind-mounts).
Correctif : Redémarrez/recréez les conteneurs ; assurez-vous de ne pas avoir bind-mounté /etc/resolv.conf involontairement ; vérifiez avec docker exec cat /etc/resolv.conf.
Listes de contrôle / plan étape par étape
Checklist 1 : Vérifier le mode de défaillance en 5 minutes
- Depuis l’hôte : résolvez le nom en échec avec
getent hostsetdigpour confirmer l’état de l’hôte. - Depuis le conteneur :
cat /etc/resolv.confet cherchez127.0.0.53ou des search/ndots suspects. - Depuis le conteneur : interrogez un résolveur connu directement avec
dig @IPpour séparer “chemin DNS” de “config DNS”. - Depuis l’hôte : exécutez
resolvectl statuspour trouver les serveurs en amont réels et les domaines split DNS. - Décidez : avez-vous besoin d’un DNS statique (serveurs) ou d’un comportement dynamique/split (forwarder) ?
Checklist 2 : Appliquer un correctif stable sur les serveurs (chemin recommandé)
- Choisissez des serveurs DNS joignables depuis les réseaux de conteneurs (souvent des résolveurs internes).
- Définissez
dnsetdns-searchdu démon Docker dans/etc/docker/daemon.json. - Redémarrez Docker pendant une fenêtre de maintenance.
- Redémarrez/recréez les conteneurs pour qu’ils récupèrent les changements.
- Exécutez un petit conteneur test DNS dans CI/CD et sur le nœud comme gate de readiness.
Checklist 3 : Appliquer un correctif stable sur les laptops avec churn VPN
- Arrêtez d’essayer d’aligner manuellement Docker parfaitement avec l’état par-lien de systemd-resolved.
- Exécutez un forwarder DNS local lié à une adresse que les conteneurs peuvent atteindre (gateway du bridge ou IP de l’hôte).
- Pointez le démon Docker vers ce forwarder.
- Rechargez éventuellement le forwarder sur les événements de connexion/déconnexion VPN.
- Gardez une image diagnostic et testez la résolution interne + externe après les changements réseau.
Checklist 4 : Gestion du changement pour prévenir la réapparition
- Codifiez la configuration DNS Docker (daemon.json) dans un outil de gestion de configuration.
- Surveillez les changements inattendus de la cible du lien symbolique
/etc/resolv.conf. - Documentez si vous comptez sur le split DNS et quels domaines internes importent.
- Ajoutez un petit canari qui résout au moins un nom interne et un nom externe depuis un conteneur sur chaque nœud.
- Lors de la mise à jour des images de base, validez le comportement de systemd-resolved avant un déploiement large.
FAQ
Pourquoi 127.0.0.53 fonctionne sur l’hôte mais pas dans les conteneurs ?
Parce que les conteneurs s’exécutent dans leur propre namespace réseau. Le loopback est par namespace. 127.0.0.53 à l’intérieur du conteneur pointe vers le conteneur lui-même, pas vers le stub systemd-resolved de l’hôte.
Pourquoi certains conteneurs montrent 127.0.0.11 à la place ?
C’est le serveur DNS embarqué de Docker, couramment utilisé sur les réseaux bridge définis par l’utilisateur. Il fournit la résolution des noms de conteneurs et relaie les requêtes externes en amont.
Si Docker utilise le DNS embarqué, pourquoi me soucier de systemd-resolved ?
Parce que le DNS embarqué est un forwarder. Il a toujours besoin de résolveurs en amont. Si Docker a capturé de mauvais paramètres amonts (comme l’adresse stub) ou des paramètres obsolètes (pré-VPN), vous perdez quand même la résolution.
Dois-je désactiver systemd-resolved pour réparer le DNS Docker ?
Généralement non. systemd-resolved est utile ; le problème est d’exporter son config stub dans les conteneurs. Préférez configurer explicitement le DNS de Docker ou utiliser le fichier resolv.conf amont. Désactiver resolved peut créer de nouveaux problèmes, surtout avec le split DNS et les network managers modernes.
Repointer /etc/resolv.conf est-il sûr ?
Ça peut l’être, mais cela change le comportement de l’hôte. Sur certains systèmes, des outils s’attendent à la configuration stub. Si vous le faites, validez le comportement de résolution de l’hôte et faites-le via un changement géré, pas un bricolage à 2 h du matin.
Dois-je redémarrer Docker après avoir changé les paramètres DNS ?
Oui, pour les changements au niveau du démon. Et vous devez généralement redémarrer ou recréer les conteneurs pour qu’ils récupèrent le nouveau contenu de resolv.conf.
Pourquoi le DNS casse-t-il seulement après la connexion au VPN ?
Parce que le VPN injecte souvent des serveurs DNS et des domaines de recherche dynamiquement. systemd-resolved met à jour sa configuration par-lien, mais Docker ne reconfigure pas automatiquement les conteneurs en cours d’exécution et ne rafraîchit pas toujours les paramètres amont à moins d’être redémarré ou guidé explicitement.
Puis-je simplement définir le DNS Docker sur un résolveur public et passer à autre chose ?
Dans les environnements d’entreprise, cela casse souvent la résolution interne et peut violer des politiques. Même en dehors d’environnements d’entreprise, cela peut masquer le vrai problème (comme des problèmes MTU) et créer de nouveaux modes de panne.
Comment savoir si j’ai des problèmes DNS liés à MTU/fragmentation ?
Les symptômes sont des timeouts intermittents, surtout pour les enregistrements à réponses volumineuses. Utilisez dig et comparez le comportement UDP vs TCP. Si TCP réussit de façon consistante alors qu’UDP timeoute, suspectez MTU/fragmentation ou des pertes causées par le pare-feu.
Quelle est l’approche la plus robuste “set and forget” ?
Sur les serveurs : configurez le démon Docker avec des résolveurs internes connus (ou un forwarder local) et gérez cela. Sur les laptops avec churn VPN : un forwarder local + Docker pointant dessus l’emporte généralement.
Conclusion : prochaines étapes réalisables aujourd’hui
Les « mystères » DNS de Docker sont souvent juste une rencontre entre namespaces et systemd-resolved. La correction consiste à arrêter de laisser les conteneurs hériter d’une adresse de résolveur stub loopback, et à choisir une source de vérité stable pour le DNS en amont.
- Exécutez le plan de diagnostic rapide et confirmez si
127.0.0.53s’infiltre dans les conteneurs. - Choisissez une stratégie : configuration DNS au niveau du démon (la plus courante), repointer
/etc/resolv.confvers le fichier amont (avec précaution), ou ajouter un forwarder local pour le split DNS et le churn VPN. - Faites en sorte que ça tienne : gérez la config, redémarrez Docker délibérément et ajoutez un test DNS basé sur un conteneur comme gate pour que cette classe de panne cesse de se répéter.
Si vous ne faites qu’une seule chose : inspectez /etc/resolv.conf à l’intérieur d’un conteneur en échec avant de redémarrer quoi que ce soit. Il est étonnant de voir à quel point ce seul fichier explique souvent tout l’incident.