Puerto Docker publicado pero inaccesible: la checklist real (Sin conjeturas)

¿Te fue útil?

Iniciaste el contenedor. Publicaste el puerto. docker ps muestra con suficiencia 0.0.0.0:8080->80/tcp.
Y, sin embargo, tu navegador se queda esperando como si esperara el autobús bajo la lluvia.

“Puerto publicado pero inaccesible” es una de esas fallas que hace que la gente inteligente haga tonterías: reiniciar Docker, alternar reglas de firewall al azar,
reconstruir imágenes y lanzar amenazas al NAT. Para. Esto es un sistema determinista. Si un puerto publicado no se puede alcanzar,
algo específico está bloqueando el paquete o la aplicación no está escuchando donde crees que lo está.

El modelo mental: qué significa realmente “publicado”

Cuando publicas un puerto con Docker (-p 8080:80), no estás “abriendo un puerto dentro del contenedor.”
Estás pidiendo al motor de Docker que organice el tráfico para que los paquetes que lleguen al puerto del host (8080) se reenvíen al puerto del contenedor (80).
Ese reenvío puede implementarse de varias maneras según la plataforma y el modo:

  • Docker en Linux con privilegios (clásico): reglas NAT de iptables/nftables (DNAT) más un pequeño proxy en espacio de usuario en algunos escenarios.
  • Docker rootless en Linux: con frecuencia usa slirp4netns / reenvío en espacio de usuario, con restricciones y características de rendimiento distintas.
  • Docker Desktop (Mac/Windows): hay una VM, y el reenvío de puertos atraviesa la frontera host ↔ VM, con una capa extra de “diversión”.

“Publicado” significa que Docker creó la intención. No garantiza que el paquete sobreviva:
el firewall del host, los grupos de seguridad en la nube, enlace a la interfaz equivocada, ruta faltante, desajuste con un proxy inverso o un proceso que escucha solo en 127.0.0.1
pueden producir el mismo síntoma: un puerto que parece abierto pero se comporta como una pared de ladrillo.

El truco diagnóstico es dejar de tratarlo como “redes de Docker” y empezar a tratarlo como un problema de ruta:
cliente → red → interfaz del host → firewall → NAT → veth del contenedor → proceso del contenedor.
Encuentra el primer lugar donde la realidad diverge de la expectativa.

Una idea parafraseada de Werner Vogels (CTO de Amazon): todo falla eventualmente; diseña para poder detectar, aislar y recuperar rápidamente.
Los puertos publicados no son diferentes: instrumenta la ruta y la verdad sale a la luz.

Guía rápida de diagnóstico (primero/segundo/tercero)

Primero: confirma que el servicio realmente está escuchando (dentro del contenedor)

Si la app no está escuchando en el puerto que mapeaste, Docker puede reenviar paquetes todo el día y aún así obtendrás timeouts o resets.
No empieces por iptables. Empieza por el proceso.

Segundo: valida que el host esté escuchando y reenviando (en la interfaz correcta)

Comprueba que el puerto del host esté ligado, en qué interfaz está ligado y si Docker insertó las reglas NAT esperadas.
Si el host no está escuchando, o solo escucha en 127.0.0.1, los clientes remotos no podrán conectarse.

Tercero: elimina “bloqueadores externos” (firewalls, grupos de seguridad en la nube, enrutamiento)

Si localhost funciona pero lo remoto no, deja de culpar a Docker. Eso es un problema de perímetro: política de UFW/firewalld/nftables, grupo de seguridad en la nube,
enrutamiento del host o una comprobación de salud del balanceador que golpea al sitio equivocado.

Cuarto: revisa modos especiales y características límite

Docker rootless, IPv6, network host, macvlan, redes overlay, proxies inversos y NAT hairpin traen cada uno sus aristas afiladas.
Si estás en uno de esos, sé explícito y sigue la rama correspondiente en la checklist más abajo.

Chiste #1: NAT es como la política de oficina—todo el mundo dice que lo entiende, y luego los ves culpar a la impresora.

Tareas prácticas: comandos, salidas, decisiones (12+)

Estos son cheques de grado producción. Cada uno incluye un comando, salida típica, qué significa y la decisión que tomas a continuación.
Ejecútalos en orden hasta encontrar la primera cosa “mal”. Ese es el punto donde te detienes y arreglas.

Tarea 1: Confirma el mapeo que Docker cree que creó

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}'
NAMES          PORTS
web-1          0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
db-1           5432/tcp

Significado: Docker afirma que publicó 8080 en todas las interfaces IPv4 y también en IPv6.
Si solo ves 127.0.0.1:8080->80/tcp, está ligado a localhost y las conexiones remotas fallarán.

Decisión: Si el mapeo se ve mal, arregla la configuración de run/compose primero. Si se ve bien, continúa.

Tarea 2: Inspecciona los bindings de puertos del contenedor (verdad fundamental)

cr0x@server:~$ docker inspect -f '{{json .NetworkSettings.Ports}}' web-1
{"80/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}

Significado: La configuración del contenedor de Docker dice: host 0.0.0.0:8080 reenvía a contenedor 80/tcp.
Si aparece null para el puerto, no está publicado.

Decisión: Si el binding no es lo esperado, redepliega con el -p correcto o con ports: en compose.

Tarea 3: Verifica que el servicio esté escuchando dentro del contenedor

cr0x@server:~$ docker exec -it web-1 sh -lc "ss -lntp | sed -n '1,6p'"
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:80         0.0.0.0:*     users:(("nginx",pid=1,fd=6))

Significado: Algo (nginx) está escuchando en 0.0.0.0:80 dentro del contenedor.
Si ves solo 127.0.0.1:80, generalmente aún funcionará porque el tráfico llega localmente dentro del contenedor,
pero algunas aplicaciones se ligan de forma extraña con solo IPv6 o sockets unix.

Decisión: Si nadie está escuchando, arregla la app/contenedor (comando equivocado, crash loop, configuración).
Si está, continúa.

Tarea 4: Hacer curl al servicio desde dentro del contenedor

cr0x@server:~$ docker exec -it web-1 sh -lc "apk add --no-cache curl >/dev/null 2>&1; curl -sS -D- http://127.0.0.1:80/ | head"
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:12 GMT
Content-Type: text/html

Significado: La app responde localmente. Si esto falla, detente. Publicar puertos no arreglará una aplicación rota.

Decisión: Si el curl interno falla, diagnostica la aplicación. Si tiene éxito, muévete hacia afuera.

Tarea 5: Hacer curl vía la IP del contenedor desde el host

cr0x@server:~$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web-1
172.17.0.4
cr0x@server:~$ curl -sS -D- http://172.17.0.4:80/ | head
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:20 GMT
Content-Type: text/html

Significado: El host puede alcanzar el contenedor a través de la red bridge. Si esto falla, el problema está dentro de la red host/contenedor
(bridge caído, enrutamiento por política, cadena DOCKER-USER, módulos de seguridad, o el contenedor adjunto a otra red).

Decisión: Si host → IP del contenedor falla, inspecciona docker network, iptables y políticas del host. Si tiene éxito, verifica la ruta de publicación.

Tarea 6: Hacer curl vía el puerto publicado del host desde el host

cr0x@server:~$ curl -sS -D- http://127.0.0.1:8080/ | head
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:28 GMT
Content-Type: text/html

Significado: El reenvío de puertos funciona localmente. Si localhost funciona pero lo remoto no, ahora estás en territorio de firewall/interfaz.

Decisión: Si localhost falla, inspecciona la escucha del host y las reglas NAT a continuación. Si localhost funciona, salta a comprobaciones de perímetro.

Tarea 7: Ver qué está realmente escuchando en el puerto del host

cr0x@server:~$ sudo ss -lntp | grep ':8080'
LISTEN 0      4096         0.0.0.0:8080      0.0.0.0:*    users:(("docker-proxy",pid=2314,fd=4))

Significado: El host tiene un listener, a menudo docker-proxy. En configuraciones más nuevas, puede que no veas docker-proxy,
porque DNAT es suficiente; entonces ss puede no mostrar nada aunque funcione.

Decisión: Si ves solo 127.0.0.1:8080, arregla el binding (ports en compose, o IP explícita del host).
Si no ves nada y falla, comprueba reglas NAT y la configuración del daemon de Docker.

Tarea 8: Confirma que Docker insertó reglas NAT (vista legacy de iptables)

cr0x@server:~$ sudo iptables -t nat -S DOCKER | sed -n '1,6p'
-N DOCKER
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.4:80

Significado: Los paquetes que llegan al TCP/8080 del host desde interfaces distintas a docker0 son DNATeados a la IP del contenedor/80.
Si falta la regla, Docker no está programando NAT (común con modo rootless, flags personalizados del daemon o incompatibilidades con nftables).

Decisión: Si las reglas faltan o son incorrectas, arregla el backend de red de Docker o redepliega Docker con integración adecuada de iptables.

Tarea 9: Revisa la cadena DOCKER-USER (la cadena “te bloqueaste a ti mismo”)

cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j DROP

Significado: Este host descarta el tráfico reenviado antes de las propias reglas de Docker. Esto hará que los puertos publicados sean inaccesibles desde la red
mientras que localhost podría seguir funcionando (dependiendo de la ruta).

Decisión: Reemplaza descartes globales por reglas de permiso explícitas, o mueve la política a una capa de firewall controlada que tenga en cuenta Docker.

Tarea 10: Si usas nftables, inspecciona el ruleset (vista moderna)

cr0x@server:~$ sudo nft list ruleset | sed -n '1,40p'
table ip nat {
  chain PREROUTING {
    type nat hook prerouting priority dstnat; policy accept;
    tcp dport 8080 dnat to 172.17.0.4:80
  }
  chain OUTPUT {
    type nat hook output priority -100; policy accept;
    tcp dport 8080 dnat to 172.17.0.4:80
  }
}

Significado: Existe DNAT en prerouting y output (conexiones locales del host).
Si la regla existe solo en OUTPUT, el tráfico remoto no se reenviará; si solo está en PREROUTING, el comportamiento en localhost puede diferir.

Decisión: Asegura que Docker esté correctamente integrado con nftables y que no estés mezclando backends incompatibles de iptables.

Tarea 11: Verifica el forwarding del kernel y ajustes bridge netfilter

cr0x@server:~$ sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables 2>/dev/null
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1

Significado: El forwarding está habilitado y el tráfico del bridge es visible para iptables.
Algunas baselines endurecidas deshabilitan esto y luego se preguntan por qué los contenedores no se pueden alcanzar.

Decisión: Si ip_forward=0 y esperas enrutamiento/NAT, actívalo (y documenta esto en tu baseline).

Tarea 12: Comprueba el estado de UFW y si está bloqueando Docker silenciosamente

cr0x@server:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

Significado: “deny (routed)” es el clásico asesino de contenedores si te basas en tráfico reenviado.
UFW puede bloquear el reenvío del bridge de Docker incluso cuando permites el puerto en el host.

Decisión: O configura UFW para permitir tráfico enrutado para las redes de Docker, o administra el firewall con reglas explícitas de iptables/nft.

Tarea 13: Revisa zonas de firewalld y masquerade (común en RHEL/CentOS)

cr0x@server:~$ sudo firewall-cmd --state
running
cr0x@server:~$ sudo firewall-cmd --get-active-zones
public
  interfaces: eth0
docker
  interfaces: docker0

Significado: firewalld puede colocar docker0 en su propia zona. Si esa zona no permite forwarding/masquerade, los puertos publicados fallan.

Decisión: Asegura que la zona docker permita forwarding según se requiera, o unifica las zonas intencionalmente.

Tarea 14: Prueba desde un host remoto y compara comportamiento de ruta

cr0x@server:~$ curl -sS -m 2 -D- http://$(hostname -I | awk '{print $1}'):8080/ | head
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:39 GMT
Content-Type: text/html

Significado: Esto simula “no localhost” usando la IP del host. Si esto falla pero 127.0.0.1 funciona,
tu binding/firewall/ruta difiere entre loopback e interfaz externa.

Decisión: Si falla, inspecciona IP de binding y firewall por interfaz. Si tiene éxito, el problema podría estar fuera del host (SG en la nube, LB, ruta del cliente).

Tarea 15: Captura de paquetes en el host para ver si llega el SYN

cr0x@server:~$ sudo tcpdump -ni eth0 tcp port 8080 -c 5
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:02:01.123456 IP 203.0.113.10.51922 > 192.0.2.20.8080: Flags [S], seq 123456789, win 64240, options [mss 1460,sackOK,TS val 1 ecr 0,nop,wscale 7], length 0

Significado: Si ves SYNs llegando, la ruta de red hasta el host está bien. Si no ves nada, el problema está aguas arriba
(grupo de seguridad, NACL, router, balanceador, DNS apuntando a otro lado).

Decisión: No hay SYN: deja de depurar Docker y ve hacia afuera. SYN llega: sigue depurando firewall/NAT/respuesta de la app en el host.

Tarea 16: Captura de paquetes en docker0 para confirmar que ocurre el reenvío

cr0x@server:~$ sudo tcpdump -ni docker0 tcp port 80 -c 5
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:02:01.124001 IP 203.0.113.10.51922 > 172.17.0.4.80: Flags [S], seq 123456789, win 64240, options [mss 1460,sackOK,TS val 1 ecr 0,nop,wscale 7], length 0

Significado: El paquete fue DNATeado y llegó a docker0. Si llega en eth0 pero no en docker0,
tus reglas NAT/forwarding son el cuello de botella.

Decisión: Arregla iptables/nftables, ajustes de forwarding o reglas en DOCKER-USER.

Dónde falla: los modos reales de fallo

1) La app escucha en la interfaz o puerto equivocado

Muchos frameworks por defecto usan 127.0.0.1 por “seguridad”. Eso está bien en portátiles. En contenedores es un autosabotaje frecuente.
Node, servidores de desarrollo de Python y algunos microframeworks en Java son infractores habituales.

Qué ves: el contenedor está “saludable”, el puerto está publicado, pero las conexiones cuelgan o se reinician. El curl dentro del contenedor puede funcionar solo vía localhost,
o no funcionar si se liga a un socket UNIX.

Qué hacer: fuerza a la app a ligarse a 0.0.0.0 (o a la interfaz del contenedor explícitamente) y confirma con ss -lntp.
Si usas un servidor de desarrollo, deténlo. Usa un servidor real (gunicorn/uvicorn, nginx, etc.) para cualquier cosa fuera de tu laptop.

2) El puerto está publicado solo a localhost

Compose y docker run permiten bindings como 127.0.0.1:8080:80. Eso significa exactamente lo que dice.
Funciona desde el host, falla desde la red, y consume horas porque “funciona en mi máquina” es un sedante potente.

Solución: liga a 0.0.0.0 o a la IP de la interfaz externa específica, intencionalmente.
Haz el binding explícito en Compose cuando lo desees.

3) El firewall del host bloquea tráfico reenviado (UFW/firewalld) aunque el puerto parezca abierto

Un punto sutil: un “puerto publicado” a menudo se implementa con NAT + forwarding.
Los firewalls pueden permitir INPUT a TCP/8080 pero bloquear FORWARD a docker0, resultando en timeouts.
Localmente puede seguir funcionando porque el tráfico de localhost puede seguir una cadena/hook diferente.

Si ejecutas UFW con “deny routed”, asume que está involucrado hasta demostrar lo contrario.
Si ejecutas firewalld, asume que zonas/mascarade son relevantes.

4) La cadena DOCKER-USER lo bloquea (intencional o accidentalmente)

DOCKER-USER existe específicamente para que puedas insertar políticas antes de las propias cadenas de Docker. Eso es buena ingeniería.
También es donde viven los “drops” temporales por años.

Un solo -j DROP en DOCKER-USER puede convertir tus puertos publicados en agujeros negros. No esparzas descartes globales a menos que también documentes globalmente.

5) Estás mezclando backends de iptables (legacy vs nft) y Docker programa el “equivocado”

En algunas distribuciones, iptables es una capa de compatibilidad sobre nftables, y puedes acabar con reglas insertadas en una vista
mientras que los paquetes son evaluados por la otra, dependiendo de versiones y configuración de kernel/userspace.

Síntoma: Docker afirma que los puertos están publicados; las reglas aparecen en iptables -t nat -S pero no afectan el tráfico,
o las reglas aparecen en nft pero la salida de iptables parece vacía.

Solución: elige un backend consistente y configura Docker en consecuencia. También: deja de tratar la pila de firewall como una novela de elige-tu-propia-aventura.

6) Docker rootless: mecánica de reenvío distinta, sorpresas distintas

Docker rootless evita redes con privilegios. Genial para postura de seguridad, menos genial para “debe comportarse como Docker con privilegios”.
Los puertos publicados se implementan vía reenvío en espacio de usuario; el rendimiento, comportamiento de binding e interacciones con firewalls difieren.

Síntoma: los puertos funcionan solo en localhost, o solo para puertos altos, o fallan al ligar a direcciones específicas.
Las reglas no aparecerán en iptables del sistema porque rootless no las programa.

Solución: confirma que estás en modo rootless, luego sigue la guía específica para rootless (como configuración explícita de reenvío de puertos).
Si necesitas comportamiento NAT clásico, ejecuta Docker con privilegios en hosts endurecidos en vez de una solución a medias.

7) Desajuste con un proxy inverso (upstream equivocado, red equivocada, expectativas TLS)

Publicas un puerto, pero el tráfico en realidad pasa por nginx/HAProxy/Traefik en el host o en otro contenedor.
Tu problema podría ser que el proxy habla con la IP equivocada del contenedor, la red equivocada, o espera TLS en HTTP plano (o viceversa).

Síntoma: el contenedor funciona vía curl directo, pero el proxy devuelve 502/504.
La gente etiqueta esto como “puerto inaccesible” porque el síntoma para el usuario es “sitio caído.”

Solución: prueba la conectividad upstream desde el contexto del proxy, no desde las sensaciones de tu portátil.

8) Hairpin NAT / “conectarme a mi IP pública desde el mismo host”

Estás en el host y haces curl a la IP pública del host:8080 y falla, pero localhost funciona.
Eso suele ser hairpin NAT. Algunas configuraciones de red (especialmente con rp_filter estricto o cierto enrutamiento en la nube)
tratan esta ruta de forma distinta.

Solución: prueba en la interfaz correcta y entiende si estás atravesando un router/LB externo y volviendo.
Si necesitas hairpin, configúralo explícitamente (o no dependas de él).

9) IPv6 está “publicado” pero no es realmente alcanzable

Docker puede mostrar bindings [::]:8080. Eso no significa que tu host tenga conectividad IPv6,
que tu firewall lo permita, o que la ruta al contenedor sea IPv6-lista.

Síntoma: IPv4 funciona; IPv6 hace timeout. O los clientes prefieren IPv6 y fallan aunque IPv4 funcionaría.

Solución: confirma enrutamiento IPv6 y reglas de firewall, y sé explícito sobre si soportas IPv6. IPv6 accidental es un pasatiempo, no una estrategia.

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

1) “Funciona en localhost pero no desde otra máquina”

Causa raíz: puerto ligado solo a 127.0.0.1, o firewall permite local pero bloquea externo, o grupo de seguridad en la nube bloquea.

Solución: publica a 0.0.0.0 (o la interfaz correcta) y abre el puerto en la capa adecuada (firewall del host + SG en la nube).

2) “docker ps muestra 0.0.0.0:PUERTO, pero ss no muestra nada escuchando”

Causa raíz: confiar en DNAT de iptables sin proxy en espacio de usuario; ss no mostrará un listener aunque NAT funcione.

Solución: prueba con curl 127.0.0.1:PUERTO. Si falla, inspecciona iptables/nft. No trates la salida de ss como la única verdad.

3) “Rechazo de conexión inmediato”

Causa raíz: ningún proceso escuchando dentro del contenedor, puerto del contenedor equivocado, o contenedor se estrelló y fue reemplazado.

Solución: docker exec ss -lntp y docker logs. Confirma que la app liga el puerto esperado.

4) “Timeout (SYN enviado, sin SYN-ACK)”

Causa raíz: paquete descartado por firewall, grupo de seguridad, DOCKER-USER, o enrutamiento/NACL.

Solución: tcpdump en la interfaz externa del host. Si el SYN nunca llega, es upstream. Si llega, inspecciona firewall/NAT en el host.

5) “Funciona del host a la IP del contenedor, pero no vía puerto publicado”

Causa raíz: NAT/forwarding no programado o bloqueado; incompatibilidad de backend de iptables; política en DOCKER-USER.

Solución: inspecciona reglas NAT, DOCKER-USER y sysctls de forwarding. Haz la política de firewall explícita y probada.

6) “Solo algunos clientes pueden alcanzarlo (otros hacen timeout)”

Causa raíz: problemas de MTU (VPNs), enrutamiento asimétrico, preferencia IPv6, o DNS de horizonte dividido apuntando a IPs diferentes.

Solución: captura paquetes; prueba forzando IPv4/IPv6; verifica MTU y rutas; no asumas que la red es una entidad uniforme.

7) “Se rompió tras activar hardening de UFW/firewalld”

Causa raíz: tráfico enrutado/reenvío bloqueado; docker0 puesto en una zona restrictiva.

Solución: permite explícitamente el forwarding para las redes Docker, o implementa reglas conscientes de Docker en DOCKER-USER con cuidado.

8) “El proxy inverso devuelve 502 pero el puerto directo funciona”

Causa raíz: upstream del proxy apunta a la IP/red equivocada del contenedor, protocolo equivocado (HTTP vs HTTPS), o DNS resuelve distinto dentro del proxy.

Solución: prueba la conectividad desde el contenedor/host del proxy, verifica objetivos upstream y asegura que proxy y app compartan la misma red Docker si usas nombres DNS entre contenedores.

Listas de verificación / plan paso a paso (haz esto, no intuición)

Checklist A: Estás en un host Linux y el puerto está muerto desde todas partes

  1. Dentro del contenedor: confirma proceso escuchando en el puerto esperado con ss -lntp.
  2. Dentro del contenedor: curl 127.0.0.1:PORT (o equivalente) para validar la respuesta de la aplicación.
  3. Host a IP del contenedor: curl CONTAINER_IP:PORT. Si falla, arregla la red de Docker o la app.
  4. Host vía puerto publicado: curl 127.0.0.1:PUBLISHED. Si esto falla pero lo anterior funciona, es NAT/forwarding.
  5. Reglas NAT: inspecciona iptables -t nat -S DOCKER o nft list ruleset.
  6. Cadenas de política: inspecciona iptables -S DOCKER-USER y cualquier política global FORWARD.
  7. Ajustes del kernel: comprueba net.ipv4.ip_forward y configuraciones de bridge netfilter.
  8. Sólo entonces: reinicia Docker si cambiaste backends de reglas o configuración del daemon. No reinicies todo como herramienta diagnóstica.

Checklist B: Localhost funciona, remoto falla

  1. Binding: verifica que no esté 127.0.0.1:PUBLISHED en docker ps / inspect.
  2. Prueba “externa” localmente: curl a la IP del host en lugar de localhost.
  3. Firewall: revisa políticas de UFW/firewalld para tráfico enrutado/reenvío.
  4. Presencia de paquetes: ejecuta tcpdump en la interfaz externa durante una prueba remota.
  5. Perímetro en la nube: confirma que grupos de seguridad/NACL/listeners del LB apunten al host/puerto correctos.
  6. Sanidad DNS: asegura que los clientes resuelvan a la IP correcta (sin registros obsoletos o sorpresa de split-horizon).

Checklist C: Es “alcanzable” pero la app está mal (502/bucles de redirección/SSL raro)

  1. Prueba directa: curl al puerto publicado directamente en el host. Obtén un 200 limpio (o la respuesta esperada).
  2. Ruta del proxy: prueba desde el contexto del proxy (contenedor u host) hacia el target upstream.
  3. Protocolo: verifica expectativas HTTP vs HTTPS. No hables TLS a un puerto plano.
  4. Headers: confirma que Host y X-Forwarded-Proto estén correctamente establecidos si la app los usa.
  5. Red: asegura que proxy y app compartan la misma red docker si usas nombres DNS entre contenedores.

Chiste #2: Si tu arreglo es “reinicia todo”, no lo arreglaste—echaste los dados y lo llamaste ingeniería.

Tres mini-historias del mundo corporativo

Mini-historia 1: El incidente causado por una suposición equivocada

Un equipo migró un servicio interno pequeño de VMs a Docker sobre una baseline Linux endurecida. El despliegue fue limpio: el contenedor corría, las comprobaciones de salud pasaban,
y el puerto estaba publicado. El ingeniero on-call verificó curl 127.0.0.1:PORT en el host y vio la respuesta esperada. Se dio por hecho.

Diez minutos después, los usuarios reales no podían alcanzarlo. El error no era un 500; era nada—timeouts. Eso desencadenó el ritual predecible:
redeploy, reiniciar Docker, reconstruir la imagen y finalmente “quizá es la red”. Mientras tanto, el balanceador marcó el servicio como no saludable y lo drenó.

La suposición equivocada fue sutil: “Si localhost funciona, la red debería funcionar”. En ese host, UFW estaba configurado con deny incoming por defecto (bien),
y deny routed por defecto (no bien para el reenvío de puertos de Docker). Las solicitudes a localhost nunca ejercitaron la misma política de forwarding que las solicitudes externas.
Así que el equipo había probado que la aplicación funcionaba, pero no la ruta publicada.

La solución fue aburrida y efectiva: una política de firewall documentada que permitiera tráfico enrutado al subnet específico de contenedores y puertos,
además de un paso en el runbook que exigiera una prueba curl remota (desde una bastión en la misma red) antes de declarar victoria.
Después de eso, este tipo de outage casi desapareció. No porque Docker fuera más amable—porque el equipo dejó de asumir.

Mini-historia 2: La optimización que salió mal

Otro equipo quería “máximo rendimiento” y eliminó todo lo que parecía overhead. Deshabilitaron el proxy en espacio de usuario en la configuración del daemon de Docker,
apretaron reglas de firewall y consolidaron la gestión de iptables bajo un agente de seguridad del host. En pruebas, todo pareció más rápido y limpio.
Los benchmarks lucían bien. Las diapositivas eran impecables.

Luego llegó producción. Un subconjunto de conexiones a puertos publicados empezó a fallar de forma intermitente—principalmente desde subnets específicas.
Las fallas no eran lo bastante consistentes como para ser obvias, pero sí como para arruinar el día de alguien. El incidente botaba entre “red” y “plataforma”
por más tiempo del que nadie quiere admitir.

La raíz fue una interacción entre el ciclo de refresco de reglas del agente de seguridad del host y las reglas NAT dinámicas de Docker.
Cada vez que se reemplazaban contenedores, Docker programaba DNAT; el agente reconciliaba más tarde al “estado deseado” y eliminaba lo que no reconocía.
Como el proxy estaba deshabilitado, no había ruta de fallback en espacio de usuario—solo reglas NAT. Algunas conexiones caían durante ventanas en que las reglas faltaban.

La recuperación fue dejar de tratar iptables como un juguete compartido. El equipo pasó a una política explícita: o el agente de seguridad poseía el estado del firewall con
integración consciente de Docker, o Docker poseía NAT más una política DOCKER-USER controlada. Eligieron lo segundo por simplicidad.
El rendimiento siguió bueno. La fiabilidad mejoró dramáticamente. La optimización no estaba mal; el modelo de propiedad sí.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Un grupo de plataforma operaba una flota de hosts Docker detrás de balanceadores internos. Tenían una práctica estricta: cada servicio tenía una “sonda de conectividad” estándar
ejecutada desde tres lugares—dentro del contenedor, desde el host y desde un nodo canario remoto en el mismo segmento de red.
Era mundano. También estaba documentado, automatizado y aplicado durante la respuesta a incidentes.

Una tarde, una actualización rutinaria del kernel fue seguida por una oleada de páginas “servicio inaccesible”. Empezó el pánico habitual en los canales de Slack.
Pero el on-call siguió las sondas. El curl dentro del contenedor funcionó. Host a IP del contenedor funcionó. Host a puerto publicado funcionó. El canario remoto falló.
Eso acotó el problema en minutos: no era Docker, ni la app. Era la alcanzabilidad de entrada.

Un cambio de red había alterado qué subnets podían alcanzar los hosts, y las comprobaciones de salud del balanceador ahora se originaban desde un subnet
que no estaba en la lista de permitidos. El grupo de plataforma tenía las capturas de paquetes que probaban que los SYNs nunca llegaron a la interfaz del host.
La solución fue una actualización limpia de la lista de permitidos del perímetro.

La práctica que salvó el día no fue un ajuste ingenioso. Fue una prueba disciplinada y repetible desde múltiples puntos de vista,
con expectativas documentadas. Evitó una espiral de depuración de contenedores y acortó el incidente.
Lo aburrido es una característica cuando estás on call.

Hechos interesantes y breve historia (para que dejes de adivinar)

  • La publicación de puertos original de Docker en Linux se apoyaba mucho en reglas NAT de iptables porque estaba ampliamente disponible y era rápido en kernel.
  • El “proxy en espacio de usuario” existía para manejar casos límite (como conexiones hairpin y algunos comportamientos de localhost) cuando solo DNAT no era suficiente.
  • La cadena DOCKER-USER se introdujo para que los operadores pudieran imponer política delante de las reglas que Docker gestiona automáticamente sin pelear con las actualizaciones de Docker.
  • iptables tiene dos “mundos” en Linux moderno: legacy xtables y backend nftables. Mezclarlos puede producir reglas que ves pero que el kernel no usa (para tu ruta de tráfico).
  • El “deny routed” por defecto de UFW es razonable para hosts sin contenedores pero con frecuencia rompe el forwarding de contenedores a menos que lo permitas explícitamente.
  • Docker rootless se hizo popular conforme los equipos de seguridad empujaron el principio de menor privilegio, pero cambia intencionalmente cómo se implementa y observa la red.
  • Docker Desktop en Mac/Windows siempre implica una frontera VM, así que los puertos publicados son una función de reenvío a través de virtualización, no solo una regla NAT local.
  • El comportamiento IPv6 suele estar “encendido por accidente” porque los bindings pueden mostrar [::] incluso cuando el entorno no soporta realmente alcanzabilidad IPv6 de extremo a extremo.

La lección de la historia: el comportamiento que ves es producto de decisiones de plataforma, postura de seguridad y evolución de la pila kernel/firewall.
Trata tu entorno como un sistema real con capas, no como una burbuja mágica de Docker.

Preguntas frecuentes

1) ¿Por qué docker ps muestra el puerto publicado si no funciona?

Porque es una vista de configuración, no una prueba de conectividad. Docker registró el binding y probablemente intentó programar el reenvío,
pero firewalls, enrutamiento o la aplicación pueden seguir bloqueando el tráfico real.

2) ¿Cómo sé si el problema es la app o la red?

Usa la prueba de tres saltos: dentro del contenedor (curl localhost), host a IP del contenedor, host al puerto publicado. El primer salto que falle es tu capa.

3) ¿Por qué localhost funciona pero la IP LAN del host no?

Diferentes rutas. Localhost puede golpear reglas OUTPUT o comportamiento local de DNAT; el tráfico externo golpea PREROUTING/FORWARD y está sujeto a política de firewall distinta.
También comprueba si accidentalmente te ligaste a 127.0.0.1.

4) ¿Necesito abrir el puerto en el firewall del contenedor?

Usualmente no. La mayoría de contenedores no ejecutan un firewall. Si lo haces, trátalo como un host real: permite inbound al puerto de la app.
Pero la mayoría de problemas de “puerto publicado inaccesible” son del host/perímetro, no del firewall del contenedor.

5) ¿Por qué a veces ss -lntp no muestra un listener para un puerto publicado?

Porque la publicación basada en NAT no requiere un proceso escuchando en el puerto del host. El kernel reescribe y reenvía paquetes.
Si se usa proxy en espacio de usuario, verás docker-proxy.

6) ¿Puede fallar la publicación de Docker por un desajuste iptables/nftables?

Sí. Si Docker programa reglas en un backend que no se usa efectivamente para tu tráfico, obtienes “las reglas existen” pero no hay reenvío.
Revisa ambas vistas de iptables y nft y estandariza la pila.

7) ¿Qué cambia en Docker rootless?

Rootless no puede programar libremente reglas NAT del sistema. La publicación de puertos suele depender de mecanismos en espacio de usuario.
La observabilidad (inspección de iptables) y el comportamiento (restricciones de binding, rendimiento) difieren. Confirma el modo primero.

8) ¿Cómo depuro si el host está detrás de un balanceador?

Captura paquetes en la interfaz del host mientras el LB hace sondas. Si los SYNs nunca llegan, es configuración del LB, reglas del SG o enrutamiento.
Si los SYNs llegan pero no alcanzan docker0, es firewall/NAT del host.

9) ¿Por qué funciona en IPv4 pero falla en IPv6?

Porque la alcanzabilidad IPv6 requiere enrutamiento de extremo a extremo y reglas de firewall. Docker mostrar [::] no asegura que tu red lo soporte.
Prueba explícitamente con IPv4/IPv6 y configura intencionalmente.

10) ¿Debería usar --network host para “evitar problemas de red de Docker”?

Solo si entiendes los tradeoffs. El networking host elimina la capa NAT pero aumenta riesgo de colisiones de puertos y reduce aislamiento.
Es una herramienta, no un vendaje para problemas desconocidos.

Conclusión: siguientes pasos que realmente evitan repeticiones

Cuando un puerto Docker está publicado pero inaccesible, el sistema te está diciendo exactamente dónde está roto—solo tienes que interrogarlo en el orden correcto.
Empieza dentro del contenedor, muévete hacia afuera y detente tan pronto como encuentres el primer salto fallido. Ese es el cuello de botella. Arregla eso, no tu paciencia.

Haz esto

  1. Codifica la prueba de tres saltos (localhost del contenedor → IP del contenedor desde el host → puerto publicado del host → canario remoto) en tus runbooks.
  2. Estandariza la propiedad del firewall: o Docker administra NAT y usas DOCKER-USER intencionalmente, o tu gestor de firewall se integra con Docker. Nada de misterio compartido.
  3. Haz los bindings explícitos en Compose (0.0.0.0:PORT:PORT vs 127.0.0.1) para no enviar “funciona en mi host”.
  4. Instrumenta la alcanzabilidad: una comprobación blackbox simple desde un nodo remoto detecta la mayoría de estos problemas antes que los usuarios.
  5. Documenta modos especiales (rootless, IPv6, proxies inversos, balanceadores) junto al servicio, no en la cabeza de alguien.

Tu objetivo no es memorizar trivia de redes de Docker. Tu objetivo es reducir el tiempo hasta la verdad. La checklist arriba lo hace—de forma fiable, repetible,
y sin necesitar una ofrenda de reinicio a los dioses de la red.

← Anterior
Proxmox Backup Server vs Veeam para VMware: qué es mejor para restauraciones rápidas y operaciones simples
Siguiente →
VPN de oficina + RDP: Escritorio remoto seguro sin exponer RDP a Internet

Deja un comentario