Docker: DNS dentro de contenedores falla — soluciones duraderas con systemd-resolved

¿Te fue útil?

El DNS dentro de los contenedores falla de la manera más desmoralizadora: no siempre. Envían una imagen, las comprobaciones de estado están verdes, y luego la aplicación no puede resolver db.internal durante dos minutos, se recupera, vuelve a fallar, y la cronología del incidente se convierte en una danza interpretativa.

En Linux moderno, esto con frecuencia no es tanto “DNS de Docker” como “el /etc/resolv.conf del host apunta al resolver stub de systemd (127.0.0.53) y Docker lo copió dentro de un espacio de red donde esa dirección no significa lo que crees”. Arreglarlo una vez es fácil. Hacer que perdure a través de reinicios, cambios de VPN, renovaciones DHCP y actualizaciones de la distro es el verdadero trabajo.

Guía rápida de diagnóstico

Este es el orden que utilizo cuando alguien dice “el DNS falla en Docker”. Está diseñado para identificar el cuello de botella rápidamente, no para satisfacer curiosidades filosóficas.

Primero: ¿es solo en el contenedor o en todo el host?

  1. Resuelve desde el host el mismo nombre por el que falla el contenedor.
  2. Resuelve desde un contenedor nuevo conectado a la misma red que el servicio que falla.
  3. Compara los servidores DNS en /etc/resolv.conf del host vs /etc/resolv.conf del contenedor.

Si el host tampoco puede resolverlo, no tienes un problema de Docker. Tienes un problema de DNS que Docker hereda educadamente.

Segundo: ¿está involucrado 127.0.0.53?

Si el /etc/resolv.conf del contenedor lista nameserver 127.0.0.53, ese es tu indicio en la mayoría de setups con systemd-resolved. Dentro del namespace de red del contenedor, 127.0.0.53 se refiere al propio contenedor, no al stub del host.

Tercero: ¿falla el DNS embebido de Docker (127.0.0.11) o los upstream?

En redes bridge definidas por el usuario, Docker proporciona un servidor DNS embebido en 127.0.0.11 que reenvía consultas a upstream. Si los contenedores no pueden resolver nombres de servicio (contenedor-a-contenedor) y tampoco nombres externos, el DNS embebido de Docker o la ruta de iptables podría estar rota. Si el descubrimiento de servicios funciona pero lo externo no, probablemente los resolvers upstream estén mal.

Cuarto: ¿estás tratando con DNS dividido?

Las VPNs y redes corporativas adoran el DNS dividido: dominios internos van a resolvers internos; el resto va al público. systemd-resolved soporta esto elegantemente. Docker… menos. Espera que un DNS “arreglado” aún pueda fallar para nombres *.corp a menos que enseñes explícitamente a Docker sobre los resolvers internos.

Quinto: revisa MTU / fallback a TCP / rarezas EDNS

Los timeouts intermitentes, especialmente a través de VPN, pueden ser paquetes DNS fragmentados o descartados. Fuerza TCP como prueba. Si TCP funciona y UDP no, estás en territorio PMTU / firewall.

Qué falla realmente: Docker, resolv.conf y systemd-resolved

Nombramos las piezas en movimiento:

  • systemd-resolved es un gestor local de resolución. Puede ejecutar un “listener stub” en 127.0.0.53, mantener servidores DNS por enlace y soportar enrutamiento DNS dividido por dominio.
  • /etc/resolv.conf es el archivo que las librerías libc leen para encontrar nameservers y dominios de búsqueda. Puede ser un archivo real o un enlace simbólico gestionado por systemd-resolved.
  • Docker genera el /etc/resolv.conf de un contenedor basándose en la configuración de resolutor del host, con algunas diferencias según el modo de red y si anulas ajustes DNS.
  • DNS embebido de Docker (127.0.0.11) existe en redes bridge definidas por el usuario. Ofrece resolución de nombres de contenedores y reenvía consultas a upstream.

El modo de fallo clásico es así:

  1. Tu distro habilita systemd-resolved.
  2. /etc/resolv.conf se convierte en un enlace simbólico a una configuración stub que apunta a nameserver 127.0.0.53.
  3. Docker ve eso y escribe el mismo nameserver dentro de los contenedores.
  4. Dentro de los contenedores, 127.0.0.53 no es el resolver del host. Es el loopback del contenedor.
  5. Las consultas tienen timeout o fallan con “connection refused”. Las aplicaciones interpretan eso como “el DNS está caído”. Están en lo cierto.

Hay un segundo modo de fallo más sutil: Docker usa 127.0.0.11 en contenedores, que reenvía a resolvers upstream. Pero upstream se llena desde la configuración del host al iniciar Docker. Entonces tu VPN se conecta, systemd-resolved cambia los servidores por enlace, y Docker sigue reenviando a los antiguos. Todo parece bien hasta que necesitas un dominio interno. Entonces es “¿por qué mi portátil lo resuelve y el contenedor no?”

Y sí, puedes parchearlo fijando 8.8.8.8 en Docker. Es el equivalente de ingeniería a tratar un dolor en el pecho con una bebida energética. Puede que deje el panel verde. También puede saltarse DNS interno, violar políticas, romper DNS dividido y convertir la depuración en producción en un desastre.

Una idea parafraseada de Dan Kaminsky (investigador DNS) que envejecerá bien: El DNS es engañosamente simple hasta que falla, y cuando falla lo hace de maneras que parecen todo lo demás. (idea parafraseada)

Un chiste, ya que estamos: el DNS es la única dependencia que puede fallar y aún convencerte de que tu app es el problema. Es como que una dirección IP te deje en visto.

Hechos e historia interesantes (lo que explica las rarezas)

  • Hecho 1: El listener stub de systemd-resolved usa 127.0.0.53 por convención, no por magia. Es solo loopback, y los namespaces cambian lo que loopback significa.
  • Hecho 2: En muchas versiones de Ubuntu, /etc/resolv.conf es un enlace simbólico a /run/systemd/resolve/stub-resolv.conf cuando systemd-resolved está habilitado.
  • Hecho 3: systemd-resolved mantiene dos variantes generadas de resolv.conf: un archivo stub (apunta a 127.0.0.53) y un archivo “upstream real” (lista los servidores DNS reales), usualmente en /run/systemd/resolve/resolv.conf.
  • Hecho 4: El DNS embebido de Docker en 127.0.0.11 no es un resolvedor recursivo de propósito general. Es un reenviador más descubrimiento de servicios y depende de que los upstream sean correctos.
  • Hecho 5: La opción ndots en resolv.conf cambia con qué frecuencia se aplican los “dominios de búsqueda”. Un ndots alto combinado con largas listas de búsqueda puede convertir una sola consulta en múltiples consultas DNS y aparente lentitud.
  • Hecho 6: El resolvedor de glibc históricamente favorece UDP y cae a TCP. Algunos firewalls descartan respuestas UDP fragmentadas, causando retrocesos lentos y timeouts que parecen intermitentes.
  • Hecho 7: Antes de que el DNS embebido de Docker se volviera común en redes definidas por usuario, el comportamiento de DNS en contenedores variaba y la gente frecuentemente vinculaba contenedores a la red del host para “arreglar DNS”, lo que a menudo creaba nuevos problemas.
  • Hecho 8: El DNS dividido (enrutamiento por dominio a resolvers específicos) es común en entornos empresariales y es una característica de primer orden de systemd-resolved; Docker no replica inherentemente el enrutamiento por dominio a menos que lo configures deliberadamente.
  • Hecho 9: Un número sorprendente de “caídas de DNS” en flotas de contenedores son en realidad sobre comportamiento de caché: el resolvedor en el contenedor cachea de forma distinta que el host, o la app cachea resultados malos y no reintenta correctamente.

Tareas prácticas: comandos, salida esperada y qué decidir

No arreglas DNS con buenas intenciones. Lo arreglas con observaciones dirigidas y decisiones. Aquí hay tareas prácticas que realmente ejecuto, con lo que significa la salida y qué hacer después.

Tarea 1: Comprueba qué piensa el host sobre /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

Significado: El archivo de resolutor del host es un enlace al stub. Espera ver 127.0.0.53 dentro.

Decisión: Probablemente necesitas que Docker use el archivo upstream real en su lugar, o configurar explícitamente los servidores DNS para Docker.

Tarea 2: Inspecciona el stub resolv.conf del host

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

Significado: El host usa el stub local de systemd. Genial en el host, tóxico si se copia a contenedores.

Decisión: No apuntes a los contenedores a 127.0.0.53. Asegúrate de que usen servidores DNS upstream reales.

Tarea 3: Inspecciona la lista “real” de resolvers del host

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

Significado: Estos son los servidores DNS upstream con los que systemd-resolved se comunica.

Decisión: Normalmente son los servidores correctos para proporcionar a Docker, ya sea mediante la config del daemon o repuntando /etc/resolv.conf (con cuidado).

Tarea 4: Confirma el estado de systemd-resolved y el listener 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

Significado: Resolved está activo y el listener stub está habilitado.

Decisión: Si deshabilitas el listener stub, debes asegurarte de que el sistema siga teniendo un resolv.conf utilizable. Deshabilitarlo a ciegas es cómo se crea una caída en todo el host.

Tarea 5: Ver DNS por enlace, incluyendo enrutamiento dividido

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

Significado: Confirma qué servidores DNS y dominios están activos. Si existe un enlace VPN, puede tener sus propios dominios y servidores.

Decisión: Si dependes de DNS dividido, no puedes simplemente “usar 1.1.1.1” y dar por terminado. Necesitas propagar resolvers internos a Docker.

Tarea 6: Comprueba qué piensa Docker sobre el DNS (vista del daemon)

cr0x@server:~$ docker info --format '{{json .}}' | jq '.DockerRootDir, .Name, .SecurityOptions'
"/var/lib/docker"
"server"
[
  "name=apparmor",
  "name=seccomp,profile=default"
]

Significado: Esto no muestra DNS directamente, pero confirma que no estás en una ruta de runtime exótica. Estamos estableciendo contexto.

Decisión: Procede a inspeccionar el resolv.conf de un contenedor y la config del daemon.

Tarea 7: Inspecciona la configuración DNS dentro de un contenedor en ejecución

cr0x@server:~$ docker exec -it web01 cat /etc/resolv.conf
nameserver 127.0.0.53
options edns0 trust-ad
search corp.example

Significado: Este contenedor apunta a sí mismo para DNS. Fallará a menos que algo dentro del contenedor escuche en 127.0.0.53:53 (no lo hará).

Decisión: Arregla la entrada DNS de Docker (config del daemon) o el enlace de /etc/resolv.conf del host para que los contenedores obtengan servidores upstream reales.

Tarea 8: Prueba funcional rápida desde dentro de un contenedor

cr0x@server:~$ docker exec -it web01 getent ahosts example.com
getent: Name or service not known

Significado: La resolución libc falla. Esto no es “curl no llega a internet”, es que la resolución de nombres falla.

Decisión: Confirma la alcanzabilidad del nameserver y si está en juego el DNS embebido de Docker.

Tarea 9: Determina si se usa el DNS embebido de Docker (127.0.0.11)

cr0x@server:~$ docker run --rm alpine:3.19 cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0

Significado: Este contenedor está en una red definida por el usuario o Docker decidió usar DNS embebido. Bien: evita la trampa de 127.0.0.53. Mal: aún necesita upstreams correctos.

Decisión: Si la resolución externa falla con 127.0.0.11, investiga el DNS upstream del daemon Docker, iptables o el firewall.

Tarea 10: Prueba la alcanzabilidad del DNS upstream desde el namespace de red del host

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

Significado: El resolver upstream es reachable y responde.

Decisión: Si los contenedores aún fallan, el problema probablemente es el reenvío contenedor→host, el reenvío DNS de Docker o NAT/iptables.

Tarea 11: Prueba la alcanzabilidad al resolver upstream desde dentro de un contenedor

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

Significado: Desde la red del contenedor, ese resolver no es reachable. Podría ser enrutamiento, firewall o accesibilidad solo vía VPN desde el namespace del host.

Decisión: Si los resolvers internos solo son reachable vía rutas del host que no están NATeadas a contenedores, necesitas arreglar enrutamiento/NAT, usar network host para ese servicio, o ejecutar un reenviador DNS que los contenedores puedan alcanzar.

Tarea 12: Inspecciona detalles de la red Docker y del contenedor

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
    }
  }
]

Significado: No hay DNS explícito en el contenedor; heredó valores por defecto. La red es un bridge definido por el usuario appnet.

Decisión: Si los valores por defecto están mal, arréglalo a nivel del daemon o por red/servicio con Compose. Prefiere el nivel del daemon cuando quieras consistencia.

Tarea 13: Comprueba el archivo de configuración del daemon Docker

cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
  "log-driver": "journald"
}

Significado: No hay configuración DNS definida. Docker tomará señales de la configuración del host y de lo que capturó al iniciarse.

Decisión: Añade servidores DNS explícitos (y opcionalmente dominios de búsqueda/opciones) si tu host usa el stub de systemd-resolved o si tus upstream cambian con frecuencia.

Tarea 14: Valida si los contenedores pueden alcanzar el puerto del DNS embebido 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

Significado: El DNS embebido responde. Si la resolución aún falla en la app, puede que tengas problemas con libc/opciones/dominios de búsqueda, no con conectividad DNS cruda.

Decisión: Inspecciona ndots, dominios de búsqueda y el cache de la aplicación.

Tarea 15: Revisa tormentas de consultas por 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

Significado: Con ndots:5, el resolvedor trata nombres cortos como “relativos” y prueba dominios de búsqueda primero. Eso puede causar fallos lentos si esos sufijos no resuelven.

Decisión: Para cargas no Kubernetes en Docker, mantén ndots modesto (a menudo 0–1) y poda dominios de búsqueda. O enseña a las aplicaciones a usar FQDNs.

Tarea 16: Captura tráfico DNS para probar dónde muere

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)

Significado: Las consultas salen por el veth del contenedor, pero no hay respuestas. Eso no es un problema de libc. Es el camino, el firewall o el upstream que te está ignorando.

Decisión: Revisa el enrutamiento al resolver desde el bridge de Docker, verifica NAT/mascaradeo y confirma que el upstream acepte consultas desde esa fuente.

Soluciones que perduran (elige una estrategia y céntrate)

Hay varias soluciones válidas. Lo inválido es mezclarlas al azar hasta que “funciona en mi portátil”. Elige una estrategia que encaje con tu entorno: portátiles con churn de VPN, servidores con resolvers estáticos o redes corporativas mixtas.

Estrategia A (la más común): Configurar el daemon Docker con servidores DNS explícitos

Este es el enfoque contundente y efectivo: dile a Docker qué servidores DNS dar a los contenedores (o qué usar como upstream para el DNS embebido). Es estable frente al stub de systemd-resolved y no depende de juegos de enlaces simbólicos de /etc/resolv.conf.

Haz esto cuando: tu entorno tiene IPs de resolvers conocidas (resolvers internos o un reenviador local) y quieres comportamiento predecible.

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

Qué observar: Los contenedores nuevos deberían mostrar esos resolvers directamente o 127.0.0.11 con reenvío correcto. Los contenedores existentes pueden necesitar reiniciarse para recoger la nueva configuración.

Contras: Muy consistente. No es ideal si tus servidores DNS son dinámicos (como los proporcionados por DHCP en portátiles). Para portátiles, considera apuntar Docker a un reenviador local que siga a systemd-resolved.

Estrategia B: Apuntar /etc/resolv.conf del host al archivo “upstream” real de systemd-resolved

Esta solución hace que Docker herede los servidores upstream reales cambiando a qué apunta /etc/resolv.conf. Es efectiva y rápida, pero también cambia el comportamiento del host. Si no entiendes las consecuencias, no lo hagas en nodos de producción sin un plan de reversión.

Haz esto cuando: quieras que Docker herede servidores DNS upstream automáticamente y aceptes saltarte el listener stub para clientes libc que leen /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

Significado: Has hecho que /etc/resolv.conf liste servidores upstream reales, no 127.0.0.53.

Decisión: Reinicia Docker para que vuelva a leer la configuración del host; luego reinicia los contenedores afectados.

cr0x@server:~$ sudo systemctl restart docker

Contras: El DNS dividido puede perder elegancia si dependías de systemd-resolved para enrutamiento por dominio en clientes stub. Algunas apps hablan con resolved vía módulos NSS; muchas solo leen resolv.conf. Conoce tu stack.

Estrategia C: Ejecutar un reenviador DNS local accesible por contenedores y apuntar Docker a él

Si tienes DNS dividido, churn de VPN o “el host puede alcanzar resolvers internos pero los contenedores no”, un reenviador local es el pegamento aburrido y correcto. El reenviador escucha en una dirección reachable desde contenedores (a menudo la gateway del bridge Docker) y reenvía a systemd-resolved o a los upstreams.

Haz esto cuando: las IPs de resolver cambian a menudo o los resolvers internos solo son alcanzables desde el namespace del host.

Un patrón práctico: ejecuta dnsmasq o unbound en el host, escuchando en 172.18.0.1 (tu gateway del bridge Docker) y reenviando a los upstreams de systemd-resolved. Luego configura el daemon Docker "dns": ["172.18.0.1"].

Por qué perdura: Docker obtiene una IP DNS estable. El reenviador puede seguir los cambios de systemd-resolved o recargarse en eventos de cambio de enlace.

Estrategia D: Usar overrides DNS a nivel de servicio en Compose con moderación

Compose te permite definir dns, dns_search, dns_opt por servicio. Es útil para excepciones, pero no es una estrategia de flota. Olvidarás el caso especial en seis meses y lo redescubrirás durante un incidente.

Úsalo cuando:

un servicio necesita un resolver especial por un periodo corto, o estás migrando.

Estrategia E: Modo de red host para cargas sensibles al DNS (último recurso)

Sí, --network host hace que muchos problemas DNS desaparezcan porque el contenedor comparte el namespace del host. También elimina el aislamiento de red y aumenta el radio de impacto. Es aceptable para un contenedor de depuración. Es último recurso para servicios en producción, salvo que tengas motivos sólidos.

Segundo chiste (y último): usar network host para arreglar DNS es como arreglar un grifo que gotea quitando la plomería. Técnicamente ya no hay goteras.

DNS dividido, VPNs y redes corporativas

El DNS dividido es donde nacen la mayoría de los tickets “funciona en el host pero no en Docker”. systemd-resolved puede enrutar consultas según sufijo de dominio a enlaces y servidores específicos. El modelo de Docker es más simple: los contenedores reciben un resolv.conf con una lista de nameservers, más dominios de búsqueda opcionales. No hay una semántica nativa en resolv.conf para “envía corp.example al resolver A y todo lo demás al resolver B” más allá del orden de servidores y la lista de búsqueda.

Qué pasa en la práctica:

  • Tu host resuelve git.corp.example porque systemd-resolved sabe que ese dominio pertenece a la interfaz VPN.
  • Tu contenedor usa 10.20.30.40 porque eso fue “global” al iniciar Docker, pero no conoce el resolver solo-VPN que llegó después.
  • Fijas un resolver público en Docker y ahora los nombres internos nunca resuelven.

Cuando estás en este mundo, la respuesta “correcta” suele ser ejecutar un reenviador DNS en el host que pueda hacer el enrutamiento dividido, y hacer que los contenedores consulten ese reenviador. systemd-resolved puede ser ese cerebro, pero los contenedores no pueden consultar de forma segura al stub en 127.0.0.53. Así que o bien:

  1. Expones un listener de resolved en una dirección no loopback (no es mi favorito), o
  2. Usas un reenviador que consulte resolved vía su archivo upstream o mediante integración D-Bus, o
  3. Empujas los mismos resolvers internos a Docker y aceptas que puedas necesitar reiniciar Docker cuando la VPN cambie.

Para portátiles, “reiniciar Docker cuando la VPN se conecta” no es elegante, pero es honesto. Automatízalo con un script dispatcher de NetworkManager si hace falta. Para servidores, prefiere resolvers estables o un reenviador local.

Tres mini-historias del mundo corporativo

1) Incidente causado por una suposición incorrecta: “127.0.0.53 siempre es el resolver del host”

Una empresa SaaS mediana tenía una flota de hosts Ubuntu ejecutando Docker. El equipo de plataforma estandarizó en systemd-resolved porque gestionaba bien clientes VPN y mantenía el estado de resolvers ordenado. Alguien vio que /etc/resolv.conf apuntaba a 127.0.0.53 y concluyó, con confianza, que todos los procesos locales—incluyendo contenedores—simplemente deberían usarlo.

Construyeron una imagen base con una comprobación de salud: ejecutar getent hosts api.corp.example. Pasó en staging de forma intermitente, lo que interpretaron como “el DNS es inestable”. Aumentaron timeouts. Añadieron reintentos. Culparon al equipo de DNS. Clásico.

El incidente de producción llegó un lunes por la mañana: varios servicios no pudieron conectar con APIs internas. El DNS externo funcionaba a veces (gracias a cache y a algunos servicios que usaban IPs directas). Los nombres internos fallaban consistentemente. Los primeros respondedores reiniciaron contenedores y vieron que algunos se recuperaban, lo que fue peor que fallar consistentemente: sugería que el problema estaba “en la app”.

La solución tomó 20 minutos una vez que alguien miró realmente el /etc/resolv.conf de un contenedor y se dio cuenta de que contenía 127.0.0.53. En el namespace del contenedor, eso es el loopback del contenedor. No había nada escuchando en el puerto 53. Actualizaron la configuración DNS del daemon Docker para usar los resolvers upstream y rodaron reinicios. El postmortem fue doloroso pero útil: las suposiciones sobre direcciones loopback no sobreviven a los namespaces.

2) Optimización que salió mal: “Reduciremos latencia DNS fijando resolvers públicos”

Un gran equipo empresarial ejecutaba estaciones de trabajo de desarrollador con setups tipo Docker Desktop en Linux. Sus servidores DNS internos eran ocasionalmente lentos en horas punta. Un ingeniero bienintencionado propuso una “optimización”: fijar el DNS del daemon Docker a resolvers públicos para mejorar el rendimiento de búsquedas externas (repos de paquetes, registries).

Pareció un acierto por una semana. Las builds fueron algo más rápidas. La gente dejó de quejarse del resolve lento de apt. Luego salió un nuevo servicio interno con un nombre que sólo existía en DNS interno. Los contenedores empezaron a fallar al resolverlo mientras el host sí lo resolvía. Los fallos fueron confusos: los desarrolladores podían curl el servicio desde el host, pero sus contenedores no.

La depuración se convirtió en echarse culpas entre equipos de app e infra. Eventualmente alguien notó que el daemon Docker había sido fijado a DNS públicos, saltándose por completo el DNS dividido. La “optimización” había eliminado silenciosamente acceso a resolución interna para todos los contenedores y en algunos entornos también violó políticas sobre logging DNS y controles de exfiltración de datos.

La reversión fue sencilla: revertir el DNS de Docker a resolvers internos y añadir un forwarder local para cachear y reducir latencia sin saltarse controles corporativos. La lección fue práctica: optimizar DNS eligiendo un resolver “rápido” es una trampa cuando tu organización usa DNS para enrutamiento y políticas.

3) Práctica aburrida pero correcta que salvó el día: “Estandarizamos pruebas DNS en CI y en hosts”

Una empresa de pagos había sufrido fallos intermitentes de DNS durante una migración previa. Respondieron con un playbook aburrido: cada nodo tenía una pequeña imagen de diagnóstico local y cada pipeline de despliegue ejecutaba una suite de saneamiento DNS antes y después del despliegue.

La suite no era sofisticada. Comprobaba que los contenedores pudieran resolver un nombre público, resolver un nombre interno y resolver un nombre de servicio en la red Docker. También verificaba la presencia de 127.0.0.53 dentro de resolv.conf del contenedor, porque ya habían visto esa película. El pipeline fallaba rápido si alguna de esas comprobaciones fallaba.

Seis meses después, una actualización de distro cambió cómo se gestionaba /etc/resolv.conf en una nueva imagen base para nodos. En la mitad de los nodos nuevos, Docker empezó a inyectar 127.0.0.53 dentro de contenedores. Las pruebas lo detectaron antes de que los servicios se desplegaran ampliamente. El incidente fue una no-evento: un pequeño lote de nodos no entró en servicio hasta que se arreglaron.

No es ingeniería glamorosa. Es la que te deja dormir tranquilo. Las pruebas diagnósticas estandarizadas no previenen todo, pero detienen las estupideces repetidas.

Errores comunes: síntoma → causa raíz → solución

1) “Los contenedores no pueden resolver nada; el host resuelve bien”

Síntoma: getent hosts example.com falla en el contenedor; funciona en el host.

Causa raíz: El /etc/resolv.conf del contenedor contiene nameserver 127.0.0.53 heredado del stub de systemd-resolved.

Solución: Configura el DNS del daemon Docker con servidores upstream reales, o repunta /etc/resolv.conf del host a /run/systemd/resolve/resolv.conf y reinicia Docker.

2) “El DNS externo funciona, los dominios internos fallan (especialmente tras conectar la VPN)”

Síntoma: example.com resuelve; git.corp.example falla en contenedores.

Causa raíz: Docker capturó servidores DNS antes de que el enlace VPN añadiera DNS dividido; los contenedores no ven los resolvers proporcionados por la VPN.

Solución: Usa un reenviador DNS local y apunta Docker a él, o reinicia Docker cuando cambie el estado de la VPN (y acepta la interrupción), o configura explícitamente servidores DNS internos en Docker.

3) “Timeouts DNS intermitentes; los reintentos ayudan”

Síntoma: Algunas consultas hacen timeout y luego tienen éxito; los logs muestran ráfagas de errores DNS.

Causa raíz: Fragmentación UDP/MTU a través de VPN; respuestas EDNS0 descartadas; firewall que bloquea UDP DNS grandes.

Solución: Prueba con consultas TCP; ajusta MTU; considera deshabilitar EDNS0 para rutas específicas; o usa un reenviador local que maneje TCP upstream.

4) “Falla el descubrimiento de servicios dentro de la red Docker”

Síntoma: El contenedor A no puede resolver el contenedor B por nombre en una red definida por usuario.

Causa raíz: La ruta del DNS embebido está rota, o el contenedor está en el bridge por defecto sin semántica de DNS embebido, o un --dns conflictivo deshabilita expectativas de descubrimiento de Docker según la configuración.

Solución: Usa una red bridge definida por el usuario; evita sobrescribir DNS por contenedor a menos que sea necesario; verifica la salud del driver de red de Docker; inspecciona reglas iptables.

5) “Las búsquedas son lentas; la CPU de la app sube en llamadas DNS”

Síntoma: Latencia aumentada; hilos de la app bloqueados en DNS; alto volumen de consultas.

Causa raíz: Lista de dominios de búsqueda desmesurada + ndots alto causando múltiples consultas por cada lookup; la app usa nombres cortos.

Solución: Reduce dominios de búsqueda; ajusta ndots apropiadamente; usa FQDNs en la configuración; considera un resolvedor con cache local.

6) “Configuramos DNS en Docker y ahora algunas apps aún usan resolvers antiguos”

Síntoma: Tras cambiar daemon.json, algunos contenedores aún tienen resolv.conf antiguo.

Causa raíz: Los contenedores existentes mantienen su resolv.conf generado hasta que se recrean/reinician (según comportamiento del runtime y bind-mounts).

Solución: Reinicia/recrea contenedores; asegúrate de que no montaste /etc/resolv.conf por error; verifica con docker exec cat /etc/resolv.conf.

Listas de verificación / plan paso a paso

Lista 1: Verifica el modo de fallo en 5 minutos

  1. Desde el host: resuelve el nombre que falla con getent hosts y dig para confirmar la salud del host.
  2. Desde el contenedor: cat /etc/resolv.conf y busca 127.0.0.53 o dominios/ndots sospechosos.
  3. Desde el contenedor: consulta un resolver conocido directamente con dig @IP para separar “ruta DNS” de “config DNS”.
  4. Desde el host: ejecuta resolvectl status para encontrar servidores upstream reales y dominios de DNS dividido.
  5. Decide: ¿necesitas DNS estático (servidores) o comportamiento dinámico/dividido (reenviador)?

Lista 2: Aplica un arreglo estable en servidores (ruta recomendada)

  1. Elige servidores DNS que sean alcanzables desde las redes de contenedores (a menudo resolvers internos).
  2. Establece dns y dns-search del daemon Docker en /etc/docker/daemon.json.
  3. Reinicia Docker en una ventana de mantenimiento.
  4. Reinicia/recrea contenedores para que recojan los cambios.
  5. Ejecuta un pequeño contenedor de prueba DNS en CI/CD y en los nodos como puerta de entrada de readiness.

Lista 3: Aplica un arreglo estable en portátiles con churn de VPN

  1. Deja de intentar mantener Docker perfectamente alineado con el estado por enlace de systemd-resolved manualmente.
  2. Ejecuta un reenviador DNS local ligado a una dirección que los contenedores puedan alcanzar (gateway del bridge o IP del host).
  3. Apunta el daemon Docker a ese reenviador.
  4. Opcionalmente recarga el reenviador en eventos de conexión/desconexión de la VPN.
  5. Mantén una imagen de diagnóstico y prueba resolución interna + externa tras cambios de red.

Lista 4: Gestión de cambios que previene incidentes repetidos

  1. Codifica la configuración DNS de Docker (daemon.json) en gestión de configuración.
  2. Monitorea cambios inesperados en el objetivo del enlace simbólico de /etc/resolv.conf.
  3. Documenta si dependes de DNS dividido y qué dominios internos importan.
  4. Añade una prueba canaria que resuelva al menos un nombre interno y uno externo desde un contenedor en cada nodo.
  5. Al actualizar imágenes base, valida el comportamiento de systemd-resolved antes de desplegar ampliamente.

Preguntas frecuentes

¿Por qué 127.0.0.53 funciona en el host pero no en contenedores?

Porque los contenedores se ejecutan en su propio namespace de red. El loopback es por namespace. 127.0.0.53 dentro del contenedor apunta al propio contenedor, no al stub de systemd-resolved del host.

¿Por qué algunos contenedores muestran 127.0.0.11 en su lugar?

Eso es el servidor DNS embebido de Docker, común en redes bridge definidas por el usuario. Proporciona resolución de nombres de contenedores y reenvía consultas a upstream.

Si Docker usa DNS embebido, ¿por qué me importa systemd-resolved?

Porque el DNS embebido solo reenvía. Aún necesita resolvers upstream. Si Docker capturó settings malos (como la dirección stub) o desactualizados (pre-VPN), seguirás perdiendo resolución.

¿Debería deshabilitar systemd-resolved para arreglar DNS de Docker?

Generalmente no. systemd-resolved está bien; el problema es exportar su configuración stub a los contenedores. Prefiere configurar explícitamente DNS en Docker o usar el resolv.conf upstream. Deshabilitar resolved puede crear nuevos problemas, especialmente con DNS dividido y gestores de red modernos.

¿Es seguro repuntar /etc/resolv.conf?

Puedes hacerlo, pero cambia el comportamiento del host. En algunos sistemas, herramientas esperan la configuración stub. Si lo haces, valida el comportamiento de resolución del host y hazlo como cambio gestionado, no como un ajuste improvisado a las 2 a.m.

¿Necesito reiniciar Docker tras cambiar settings DNS?

Sí, para cambios a nivel de daemon. Y normalmente necesitas reiniciar o recrear contenedores para que cojan el nuevo contenido de resolv.conf.

¿Por qué el DNS se rompe solo después de conectar la VPN?

Porque la VPN suele inyectar servidores DNS y dominios de búsqueda dinámicamente. systemd-resolved actualiza su configuración por enlace, pero Docker no reconfigura automáticamente contenedores en ejecución ni siempre refresca upstreams a menos que se reinicie o se le guíe explícitamente.

¿Puedo simplemente poner el DNS de Docker a un resolver público y listo?

En entornos corporativos, eso suele romper la resolución interna y puede violar políticas. Incluso fuera de empresas, puede esconder el problema real (como MTU) y crear nuevos modos de fallo.

¿Cómo detecto si tengo problemas de MTU/fragmentación en DNS?

Los síntomas son timeouts intermitentes, especialmente para registros con respuestas grandes. Usa dig y compara comportamiento UDP vs TCP. Si TCP funciona de forma consistente mientras UDP hace timeout, sospecha de MTU/fragmentación o drops por firewall.

¿Cuál es el enfoque más robusto “configura y olvida”?

En servidores: configura el DNS del daemon Docker a resolvers internos conocidos (o a un reenviador local) y mantenlo gestionado. En portátiles con churn de VPN: un reenviador local + Docker apuntando a él suele ser la mejor opción.

Conclusión: próximos pasos que puedes ejecutar hoy

Los “misterios” del DNS en Docker suelen ser simplemente namespaces encontrándose con systemd-resolved. La solución es dejar de permitir que los contenedores hereden una dirección stub solo loopback y elegir una fuente de verdad estable para el DNS upstream.

  1. Ejecuta la guía rápida de diagnóstico y confirma si 127.0.0.53 se está filtrando a los contenedores.
  2. Elige una estrategia: configuración a nivel daemon (la más común), repuntar /etc/resolv.conf al archivo upstream (con cuidado) o añadir un reenviador local para DNS dividido y churn de VPN.
  3. Haz que perdure: gestiona la configuración, reinicia Docker deliberadamente y añade una prueba basada en contenedores como puerta para que esta clase de caída deje de repetirse.

Si solo haces una cosa: inspecciona /etc/resolv.conf dentro de un contenedor que falla antes de reiniciar nada. Es asombroso cuántas veces ese único archivo explica todo el incidente.

← Anterior
Greylisting de correo electrónico: cuándo ayuda y cuándo solo retrasa el correo real
Siguiente →
Proxmox ZFS «cannot import pool»: causas raíz, comprobaciones y opciones de recuperación

Deja un comentario