“Conexión rechazada” es un mensaje maravillosamente directo. No significa que tu aplicación esté “un poco lenta hoy.”
Significa que algo intentó abrir una conexión TCP y el otro extremo dijo “no” de inmediato.
Sin handshake. Sin espera. Simplemente una puerta cerrada.
En stacks de Docker, esa puerta se cierra por razones previsibles: estás marcando la dirección equivocada, estás en la red equivocada,
estás golpeando el puerto incorrecto, el servidor no escucha donde crees que lo hace, o algo en el medio está filtrando el tráfico.
Este artículo trata de demostrar cuál es el problema, rápido—y arreglar el modelo de red en lugar de esparcir reintentos como agua bendita.
Qué significa realmente “conexión rechazada” (y lo que no significa)
“Conexión rechazada” es la forma que tiene TCP de decir: la IP de destino es alcanzable, pero nadie está escuchando en ese puerto
(o algo rechazó activamente la conexión con un RST TCP). Ese es un modo de fallo muy distinto de:
- Timeout: los paquetes desaparecen, el enrutamiento está roto, un firewall está descartando, o el servicio está bloqueado y no responde.
- Fallo de resolución de nombres: ni siquiera obtienes una dirección IP para el nombre objetivo.
- Connection reset by peer: te conectaste y luego la aplicación te cerró a mitad de la comunicación.
En Docker, “rechazado” a menudo significa que te conectaste al lugar equivocado con éxito. Suena contradictorio hasta que te das cuenta
de la frecuencia con que los desarrolladores apuntan por error a localhost, o al puerto publicado del host desde la misma red,
o a una IP de contenedor que cambió desde el martes pasado.
Aquí tienes la regla que puedes pegar en tu monitor: dentro de un contenedor, “localhost” significa el propio contenedor.
Si te conectas de un contenedor a otro, “localhost” casi siempre está mal, salvo que ejecutes intencionalmente ambos procesos
en el mismo contenedor (lo cual es otra decisión de estilo).
Un modelo mental de la red Docker para operar bajo presión
Las redes de Docker no son “difíciles.” Solo están en capas. La gente se complica cuando adivina qué capa falla.
No vamos a adivinar. Vamos a probarlo.
Layer 1: Proceso y socket
Algo debe estar escuchando en un puerto. Si tu servicio escucha en 127.0.0.1 dentro de su contenedor, otros contenedores no pueden alcanzarlo.
Debe enlazarse a 0.0.0.0 (o a la dirección de la interfaz del contenedor).
Layer 2: Espacio de nombres de red del contenedor
Cada contenedor tiene su propio espacio de nombres de red: sus propias interfaces, rutas y loopback. Los contenedores pueden adjuntarse a una o más redes.
Docker crea un par veth para conectar el espacio de nombres del contenedor a un bridge (para redes bridge) o a un overlay (para Swarm).
Layer 3: Redes Docker (bridge/overlay/macvlan)
La red “bridge” por defecto no es lo mismo que una red bridge definida por el usuario. Las redes definidas por el usuario proporcionan descubrimiento de servicios basado en DNS.
Compose se apoya en eso. Si caes de nuevo en el bridge por defecto y empiezas a hardcodear IPs, estás escribiendo incidentes futuros.
Layer 4: Descubrimiento de servicios (DNS de Docker)
En redes definidas por el usuario, Docker ejecuta un servidor DNS embebido. Los contenedores comúnmente lo ven como 127.0.0.11 en /etc/resolv.conf.
Los nombres de servicio de Compose resuelven a IPs de contenedor en esa red. Si la resolución de nombres falla, todo lo que viene después se vuelve caos.
Layer 5: Publicación de puertos en el host y NAT
ports: en Compose publica puertos del contenedor al host. Eso es para tráfico que viene desde fuera de Docker (tu portátil, el host, otras máquinas).
Dentro de la red Docker, los contenedores normalmente deberían hablar entre sí en el puerto de contenedor vía el nombre del servicio.
Si te encuentras conectándote desde el contenedor A a host.docker.internal:5432 para alcanzar el contenedor B, párate y pregúntate:
“¿Por qué estoy saliendo de la red Docker y reentrando por NAT?” A veces es necesario. La mayoría de las veces no.
Una cita, porque el mundo ops tiene recibos
Idea parafraseada de Werner Vogels (fiabilidad/arquitectura): “Todo falla; diseña asumiendo fallos y recupera mediante automatización.”
Eso aplica aquí: deja de esperar que la red se comporte; diseña para observarla y verificarla.
Guía rápida de diagnóstico (primero/segundo/tercero)
Cuando producción está ardiendo, no necesitas un doctorado en filosofía. Necesitas una secuencia que reduzca el espacio de búsqueda.
Esta guía está pensada para “el servicio A no puede conectar al servicio B” con “conexión rechazada.”
Primero: confirma que marcas el objetivo correcto desde el contenedor llamante
- Desde dentro del contenedor A: resuelve el nombre que estás usando.
- Confirma que la IP esté en una red compartida con el contenedor B.
- Intenta una conexión TCP al puerto previsto.
Si la resolución de nombres falla o apunta a un lugar inesperado, para. Arregla DNS/miembro de red primero.
Segundo: confirma que el contenedor servidor realmente está escuchando en la interfaz y puerto correctos
- Dentro del contenedor B: lista los sockets en escucha.
- Comprueba que el servicio enlaza a
0.0.0.0, no a127.0.0.1. - Revisa los logs de la aplicación por “iniciado” versus “crash y reinicio”.
Tercero: inspecciona la plomería de la red Docker y el filtrado del host
- Inspecciona las conexiones de red y las IPs de los contenedores.
- Revisa reglas de iptables/nftables si el host está involucrado (puertos publicados, o tráfico cruzando espacios de nombres).
- Busca aislamiento de red accidental (múltiples proyectos Compose, múltiples redes, alias incorrectos).
Broma #1: Si tu arreglo es “add sleep 10,” no solucionaste la red—solo negociaste con el tiempo, y el tiempo siempre te pasa factura después.
Tareas prácticas: comandos, salida esperada y decisiones
A continuación hay tareas prácticas que puedes ejecutar en un host Linux con Docker y Compose. Cada tarea incluye un comando, una salida realista,
lo que significa esa salida y la decisión que tomas. Esta es la parte que copias en tu canal de incidentes.
Task 1: Identifica los detalles de la conexión fallida (desde logs)
cr0x@server:~$ docker logs --tail=50 api
2026-01-03T09:12:41Z ERROR db: dial tcp 127.0.0.1:5432: connect: connection refused
2026-01-03T09:12:41Z INFO retrying in 1s
Significado: El contenedor API está intentando alcanzar Postgres en 127.0.0.1 dentro de su propio espacio de nombres de red.
A menos que Postgres se ejecute en el mismo contenedor, eso está mal.
Decisión: Cambia el host de la BD al nombre del servicio de Compose (por ejemplo db) y usa el puerto del contenedor (5432).
Task 2: Confirma que el estado del contenedor no te miente
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES STATUS PORTS
api Up 2 minutes 0.0.0.0:8080->8080/tcp
db Restarting (1) 5 seconds ago 5432/tcp
Significado: La BD está reiniciando. “Conexión rechazada” podría ser real (nadie escucha), no un problema de enrutamiento.
Decisión: Deja de afinar el cliente. Arregla el crash loop del contenedor servidor primero: inspecciona logs y configuración de la BD.
Task 3: Inspecciona los logs del servidor buscando problemas de bind/listen
cr0x@server:~$ docker logs --tail=80 db
2026-01-03 09:12:32.177 UTC [1] LOG: listening on IPv4 address "127.0.0.1", port 5432
2026-01-03 09:12:32.177 UTC [1] LOG: listening on IPv6 address "::1", port 5432
2026-01-03 09:12:32.180 UTC [1] LOG: database system is ready to accept connections
Significado: Postgres solo está escuchando en loopback dentro de su contenedor. Otros contenedores recibirán “rechazado.”
Decisión: Configura Postgres para escuchar en 0.0.0.0 (o en la interfaz del contenedor) y ajusta pg_hba.conf en consecuencia.
En contenedores, “seguro por enlazar a localhost” es teatro de seguridad con daños colaterales.
Task 4: Prueba la resolución de nombres dentro del contenedor llamante
cr0x@server:~$ docker exec -it api getent hosts db
172.21.0.3 db
Significado: El DNS de Docker puede resolver db a una IP. Eso es un prerrequisito para el descubrimiento de servicios.
Decisión: Si esto falla, estás en la red equivocada o usando el nombre incorrecto. Arregla redes/alias de Compose, no la aplicación.
Task 5: Prueba el puerto TCP desde el contenedor llamante (señal rápida)
cr0x@server:~$ docker exec -it api bash -lc 'nc -vz -w2 db 5432; echo exit=$?'
nc: connect to db (172.21.0.3) port 5432 (tcp) failed: Connection refused
exit=1
Significado: Alcanzaste la IP del contenedor, pero el puerto la rechazó. Eso casi siempre es “no hay nadie escuchando” o “escucha solo en loopback.”
Decisión: Investiga los sockets en escucha y la dirección de bind del contenedor servidor (siguiente tarea).
Task 6: Comprueba en qué está escuchando el servidor (dentro del contenedor servidor)
cr0x@server:~$ docker exec -it db bash -lc 'ss -lntp | head -n 20'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=1,fd=6))
Significado: Postgres está ligado a 127.0.0.1:5432 solamente. Los contenedores en la misma red no pueden conectarse.
Decisión: Reconfigura el servicio para enlazar a 0.0.0.0 (o a la IP del contenedor) y reinicia.
Task 7: Confirma que ambos contenedores comparten la misma red Docker
cr0x@server:~$ docker inspect -f '{{json .NetworkSettings.Networks}}' api
{"appnet":{"IPAMConfig":null,"Links":null,"Aliases":["api","api-1"],"NetworkID":"c0b3f6f2e2c1","EndpointID":"8d6d...","Gateway":"172.21.0.1","IPAddress":"172.21.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:15:00:02","DriverOpts":null}}
cr0x@server:~$ docker inspect -f '{{json .NetworkSettings.Networks}}' db
{"appnet":{"IPAMConfig":null,"Links":null,"Aliases":["db","db-1"],"NetworkID":"c0b3f6f2e2c1","EndpointID":"a21c...","Gateway":"172.21.0.1","IPAddress":"172.21.0.3","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:15:00:03","DriverOpts":null}}
Significado: Ambos están en appnet con el mismo NetworkID. Así que el enrutamiento entre ellos debería estar bien.
Decisión: Centra la atención en la configuración de escucha del servidor y la disponibilidad de la aplicación, no en rarezas multi-red.
Task 8: Inspecciona el objeto de red por sorpresas (subnet, contenedores, opciones)
cr0x@server:~$ docker network inspect appnet --format '{{json .IPAM.Config}} {{json .Containers}}'
[{"Subnet":"172.21.0.0/16","Gateway":"172.21.0.1"}] {"1c2f...":{"Name":"api","IPv4Address":"172.21.0.2/16"},"7aa9...":{"Name":"db","IPv4Address":"172.21.0.3/16"}}
Significado: Confirmado subnet y membresía. Si la BD no aparece listada, no está en la red que crees.
Decisión: Si la membresía es incorrecta: arregla Compose para adjuntar servicios a la misma red definida por el usuario y redeploy.
Task 9: Detecta el problema de “confusión por puerto publicado”
cr0x@server:~$ docker port db
5432/tcp -> 0.0.0.0:15432
Significado: El contenedor DB expone 5432 internamente, publicado como 15432 en el host.
Otros contenedores deberían seguir usando db:5432, no db:15432 ni localhost:15432.
Decisión: Si la configuración de la aplicación apunta a 15432 desde dentro de Docker, corrígela. Los puertos publicados son para clientes externos.
Task 10: Valida el enrutamiento desde dentro del contenedor llamante
cr0x@server:~$ docker exec -it api ip route
default via 172.21.0.1 dev eth0
172.21.0.0/16 dev eth0 proto kernel scope link src 172.21.0.2
Significado: El contenedor tiene una ruta a la subred donde vive db. Si la ruta falta, adjuntaste la red equivocada.
Decisión: Ruta faltante significa adjunto de red incorrecto. Arregla redes de Compose, no hackees /etc/hosts.
Task 11: Valida que la DNS del llamante apunte al DNS embebido de Docker
cr0x@server:~$ docker exec -it api cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0
Significado: Se está usando el DNS embebido de Docker. Si ves solo servidores DNS corporativos, la resolución de nombres a nombres de servicio puede fallar.
Decisión: Si el DNS de Docker no se usa, revisa ajustes de DNS sobrescritos en la configuración del daemon o en Compose.
Task 12: Prueba que el servicio es accesible cuando se corrige el bind
cr0x@server:~$ docker exec -it db bash -lc 'grep -E "^(listen_addresses|port)" -n /var/lib/postgresql/data/postgresql.conf | head'
60:listen_addresses = '*'
64:port = 5432
cr0x@server:~$ docker restart db
db
cr0x@server:~$ docker exec -it api bash -lc 'nc -vz -w2 db 5432; echo exit=$?'
Connection to db (172.21.0.3) 5432 port [tcp/postgresql] succeeded!
exit=0
Significado: Convertimos “rechazado” en “exitoso” arreglando el listener.
Decisión: Fija el cambio de configuración, añade una comprobación de readiness y elimina cualquier “retry” cliente sin límites.
Task 13: Atrapa la idea equivocada “depends_on significa listo”
cr0x@server:~$ docker compose ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
stack-api-1 api:latest "/app/api" api Up 20 seconds 0.0.0.0:8080->8080/tcp
stack-db-1 postgres:16 "docker-entrypoint..." db Up 22 seconds 5432/tcp
Significado: “Up” no es “listo.” Postgres puede seguir ejecutando migraciones o reproduciendo WAL. Los clientes pueden ver rechazado durante el arranque temprano.
Decisión: Añade un healthcheck a la BD y condiciona el arranque del API a la disponibilidad de la BD (o implementa reintentos robustos con backoff acotado).
Task 14: Verifica el estado de salud (cuando añades healthchecks)
cr0x@server:~$ docker inspect -f '{{.State.Health.Status}}' stack-db-1
healthy
Significado: Los healthchecks te dan una señal de readiness fiable. Puedes usarla para decisiones de orquestación y alertas.
Decisión: Si aparece unhealthy: deja de culpar a la red y arregla la inicialización de la BD, credenciales, disco o configuración.
Task 15: Detecta problemas de firewall/NAT en el host para puertos publicados (host involucrado)
cr0x@server:~$ sudo iptables -S DOCKER | head
-N DOCKER
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.21.0.2:8080
Significado: Docker inyecta reglas para reenviar puertos del host a IPs de contenedor. Si estas reglas faltan, los puertos publicados no funcionarán.
Decisión: Si las reglas faltan o tu entorno usa nftables que anulan Docker: alinea la gestión del firewall con Docker,
o usa un modo de red diferente intencionalmente. No “simplemente vacíes iptables” en producción a menos que te gusten auditorías sorpresa.
Task 16: Detecta proyectos Compose múltiples accidentalmente en redes separadas
cr0x@server:~$ docker network ls --format 'table {{.Name}}\t{{.Driver}}\t{{.Scope}}' | grep -E 'stack|appnet'
NAME DRIVER SCOPE
stack_default bridge local
billing_default bridge local
Significado: Dos proyectos, dos redes por defecto. Un contenedor en stack_default no puede alcanzar servicios en billing_default por nombre.
Decisión: Adjunta ambas stacks a una red definida por el usuario compartida (explícitamente), o ejecútalas como un solo proyecto Compose si están acopladas.
Tres mini-historias corporativas desde el frente
Mini-historia 1: El incidente causado por una suposición equivocada (“localhost es la base de datos”)
Un equipo de producto migró un monolito a “microservicios” durante un trimestre. No fue una conversión religiosa; fue una partida presupuestaria.
El primer paso fue ejecutar el API y Postgres en Docker Compose localmente, y luego promover esa configuración a un entorno de desarrollo compartido.
En los días del monolito, la base de datos vivía en la misma VM. Así que la configuración en todas partes decía DB_HOST=localhost.
Durante la migración, alguien puso Postgres en un contenedor pero mantuvo la variable antigua, pensando que Docker la “mapearía.”
Docker lo mapeó—directamente al lugar equivocado.
El síntoma fue inmediato: los contenedores API lanzaron connect: connection refused. La primera respuesta fue aumentar los reintentos,
porque el equipo había sido quemado recientemente por cold starts en Kubernetes. Los reintentos pasaron de 3 a 30, y los logs se convirtieron en una novela cara.
Seguía fallando, porque no puedes alcanzar eventual consistency con un socket que no existe.
La ruptura ocurrió cuando alguien ejecutó getent hosts dentro del contenedor y notó que el nombre de servicio db resolvía bien.
El API simplemente no lo estaba usando. Un cambio en la configuración después—DB_HOST=db—y el incidente terminó.
La lección no fue “usa nombres de servicio.” La lección fue: las suposiciones son deuda técnica con fecha de vencimiento.
Mini-historia 2: La optimización que salió mal (puertos publicados para “rendimiento”)
Otra organización tenía un entorno de integración basado en Compose. Un ingeniero senior (competente, de verdad) decidió “simplificar la red”
haciendo que los servicios se hablasen entre sí vía puertos publicados en el host. La lógica sonaba limpia:
“Todo apunta a la IP del host, usamos una sola política de firewall y será más fácil depurar.”
Funcionó hasta que dejó de funcionar. Bajo carga, empezaron a ver “conexión rechazada” intermitente del servicio A al servicio B.
El rechazo se agrupaba durante despliegues, pero también al azar en horas pico. Ese es el tipo de comportamiento que hace que la gente culpe al proveedor cloud,
al kernel y a veces a la astrología.
El problema real fue complejidad autoinfligida. El tráfico entrante dio un rodeo: contenedor A → NAT del host → docker-proxy/iptables → contenedor B.
Durante reinicios de contenedores y churn de IPs, las ventanas de tiempo se ampliaron. Algunas conexiones aterrizaron en un mapeo de puerto que momentáneamente apuntaba a ninguna parte.
Además, las llamadas internas quedaron atadas a direcciones dependientes del host, dificultando la escalabilidad horizontal y los failovers.
Revirtieron a tráfico directo servicio-a-servicio sobre la red definida por el usuario, usando nombres de servicio y puertos de contenedor.
Para depuración e ingreso mantuvieron puertos publicados, pero las llamadas internas se quedaron internas. Su “optimización” fue en realidad un rodeo por más piezas móviles.
Mini-historia 3: La práctica aburrida que salvó el día (healthchecks y redes explícitas)
Un equipo de plataforma ejecutaba una stack Compose modesta para herramientas internas: API, cola, Postgres y un worker. Nada sofisticado.
Lo que sí era sofisticado era que lo trataron como producción: redes definidas por el usuario explícitas, healthchecks y nombres de servicio previsibles.
Sin defaults mágicos. Sin “funciona en mi portátil.”
Un viernes, el host se reinició tras un parche de kernel rutinario. Los servicios volvieron, pero el API empezó a fallar de inmediato.
El on-call vio “conexión rechazada” y se preparó para una larga noche. Entonces comprobó la salud de la BD: starting.
Postgres estaba reproduciendo WAL tras un apagado no limpio—normal, pero tarda.
Como había healthchecks, el contenedor API no atacó en estampida la BD con tormentas de conexiones.
Esperó. Los logs fueron aburridos. Las alertas fueron significativas. Diez minutos después, todo estaba healthy y nadie escribió un update en pánico.
Aburrido es un logro. “Funciona tras reinicio” no es una propiedad por defecto; es algo que se ingenia.
Errores comunes: síntoma → causa raíz → solución
Estos son los patrones recurrentes detrás de “conexión rechazada” en sistemas dockerizados. Cada uno incluye una acción correctiva específica.
Si tu equipo repite uno, conviértelo en una regla de lint o ítem de la checklist de revisión.
1) “El API no puede alcanzar la BD, pero la BD está en ejecución” → DB se liga a loopback → ligar a 0.0.0.0
- Síntoma:
connect: connection refuseddesde otros contenedores;ssmuestra127.0.0.1:5432. - Causa raíz: El servicio escucha solo en loopback dentro del contenedor.
- Solución: Configurar la dirección de bind/listen a
0.0.0.0(o IP del contenedor), más reglas de autenticación apropiadas (p. ej.pg_hba.confde Postgres).
2) “Funciona en el host, falla en el contenedor” → uso de localhost → usar nombre de servicio
- Síntoma: Config de la app usa
localhosto127.0.0.1para otro servicio. - Causa raíz: Malentendido de espacios de nombres de red.
- Solución: Usar el nombre del servicio de Compose (p. ej.
redis,db) y el puerto del contenedor.
3) “Conexión rechazada solo durante el arranque” → readiness no garantizada → añadir healthchecks/backoff
- Síntoma: Primeros segundos/minutos tras deploy: rechazado; más tarde: OK.
- Causa raíz: El cliente arranca antes de que el servidor esté escuchando (o listo para aceptar conexiones).
- Solución: Healthchecks y gating de dependencias; o reintentos cliente con backoff exponencial acotado y jitter.
4) “Puedo hacer ping, pero TCP es rechazado” → la red está bien, el puerto no → deja de depurar L3
- Síntoma: IP alcanzable, ARP/enrutamiento bien, pero
ncfalla con refused. - Causa raíz: Servicio caído, puerto equivocado, dirección de bind equivocada, o crash de la app.
- Solución: Revisa
ss -lntpy logs del servicio; verifica mapeo de puertos y configuración.
5) “El nombre de servicio no resuelve” → red/proyecto equivocado → adjuntar a la misma red definida por usuario
- Síntoma:
getent hosts dbfalla dentro de un contenedor. - Causa raíz: Contenedores en redes distintas o proyectos Compose distintos sin una red compartida.
- Solución: Declara una red compartida explícita en Compose y adjunta ambos servicios a ella.
6) “Conectando al puerto publicado desde otro contenedor” → rodeo por NAT → usa puerto del contenedor en la red
- Síntoma: Contenedor llama a
host:15432oservice:15432porque 15432 está publicado. - Causa raíz: Confusión entre puertos de ingreso y puertos internos.
- Solución: Para tráfico interno:
db:5432. Publica puertos solo para clientes externos.
7) “Rechazos intermitentes tras redeploy” → suposiciones de IP obsoletas → deja de usar IPs de contenedor
- Síntoma: IP hardcodeada funciona hasta un reinicio, luego rechazado/timeout.
- Causa raíz: Las IPs de contenedor cambian; tu configuración no.
- Solución: Usa descubrimiento por nombre, no direcciones IP de contenedor.
8) “Puerto publicado muerto desde fuera” → firewall/nftables en conflicto → alinea filtrado del host con Docker
- Síntoma: Puerto del host mapeado, contenedor escucha, pero clientes externos reciben rechazado.
- Causa raíz: Reglas de firewall del host que anulan el DNAT/forwarding de Docker, o reglas de Docker que no se instalaron bien.
- Solución: Arregla la política de firewall para permitir el forwarding; asegúrate de la integración de la cadena Docker; evita gestionar iptables con dos sistemas compitiendo.
Broma #2: La red de Docker no está encantada; solo lo parece cuando te saltas la parte donde verificas en qué universo están tus paquetes.
Listas de verificación / plan paso a paso
Paso a paso: de “rechazado” a causa raíz en 10 minutos
- Identifica el objetivo exacto desde el log del cliente: host, puerto, protocolo. Si es
localhost, asume que está mal hasta que se demuestre lo contrario. - Revisa salud/estado del contenedor: ¿el servidor está reiniciando o unhealthy?
- Desde el contenedor cliente: resuelve el nombre del servidor y captura la IP.
- Desde el contenedor cliente: intenta conectar TCP con
ncal nombre y puerto del servidor. - Desde el contenedor servidor: confirma que existe un listener con
ss -lntp. - Confirma dirección de bind: si es
127.0.0.1, cambia a0.0.0.0(y ajusta autenticación). - Confirma membresía de red: ambos contenedores en la misma red definida por el usuario; inspecciona el objeto de red.
- Elimina confusión de puertos: llamadas internas usan puerto del contenedor; llamadas externas usan puerto publicado.
- Sólo entonces inspecciona reglas de firewall/NAT del host, si el host forma parte del camino.
- Tras arreglar: añade healthchecks/readiness, quita sleeps rituales y documenta el contrato de red.
Checklist de despliegue: prevenir “rechazado” antes de que ocurra
- Usa una red definida por el usuario; no confíes en el bridge por defecto.
- Usa nombres de servicio para tráfico entre servicios; nunca hardcodees IPs de contenedor.
- Enlaza servicios de red a
0.0.0.0dentro de contenedores salvo razón específica. - Publica puertos solo para ingreso; no enrutes tráfico interno a través del host.
- Añade healthchecks para servicios stateful (BD, cache, cola); consume el estado de salud en decisiones de orquestación.
- Implementa reintentos acotados con backoff y jitter en clientes; trátalo como resiliencia, no como parche.
- Mantén la política de firewall coherente con Docker; evita gestores de reglas enfrentados.
- Haz explícitas y nombradas las redes de Compose cuando múltiples proyectos deban comunicarse.
Datos interesantes y contexto histórico (útil, no trivia)
- Dato 1: Las primeras configuraciones de Docker dependían mucho de bridges Linux e iptables NAT; los puertos publicados todavía se implementan mediante reglas DNAT en muchos sistemas.
- Dato 2: La red
bridgepor defecto históricamente se comportaba distinto de los bridges definidos por usuario, especialmente alrededor del DNS/descubrimiento automático de servicios. - Dato 3: El DNS embebido de Docker suele aparecer como
127.0.0.11dentro de contenedores en redes definidas por usuario—un detalle que es oro para diagnóstico. - Dato 4: “Conexión rechazada” suele ser un RST TCP inmediato, lo que significa que el camino de red hacia la IP funcionó; el endpoint rechazó el puerto.
- Dato 5: Los nombres de servicio de Compose se convirtieron en un mecanismo de descubrimiento de facto para dev/test mucho antes de que muchos equipos adoptaran sistemas de descubrimiento “reales”.
- Dato 6: Las IPs de contenedor son intencionalmente efímeras; el direccionamiento estable se proporciona mediante nombres y descubrimiento, no fijando IPs.
- Dato 7: Algunas distros migraron de iptables a nftables; herramientas de firewall descoordinadas pueden producir fallos de red Docker confusos si las cadenas no se integran correctamente.
- Dato 8:
depends_onen Compose nunca fue una garantía de readiness; es orden. Trata “listo” como una propiedad a nivel de aplicación. - Dato 9: Enlazar a
127.0.0.1dentro de un contenedor es una trampa clásica porque silenciosamente bloquea todo el tráfico externo de contenedor mientras parece “seguro”.
Preguntas frecuentes
1) ¿Por qué obtengo “conexión rechazada” en lugar de un timeout?
Rechazado suele significar que llegaste a la IP de destino y el kernel respondió con un reset porque nadie escucha en ese puerto
(o un firewall rechazó activamente). Los timeouts tienen más que ver con paquetes descartados y rutas rotas.
2) Si ambos servicios están en Compose, ¿por qué no pueden hablar automáticamente?
Pueden, pero solo si comparten una red y usas nombres de servicio. Los problemas ocurren cuando los servicios caen en redes distintas,
proyectos Compose distintos, o el cliente está configurado para usar localhost o un puerto publicado en el host.
3) ¿Debo usar direcciones IP de contenedor por rendimiento?
No. Las IPs de contenedor cambian. La resolución de nombres no es tu cuello de botella; tu próxima caída sí lo será.
Usa nombres de servicio y deja que Docker DNS maneje el mapeo.
4) ¿Cuál es la diferencia entre expose y ports en Compose?
expose documenta puertos internos y puede influir en el comportamiento de linkage, pero no publica al host.
ports publica puertos a la interfaz del host (normalmente vía NAT). El tráfico container-to-container no necesita puertos publicados.
5) ¿Es depends_on suficiente para prevenir fallos de conexión al arrancar?
No. Lanza contenedores en orden; no garantiza que la dependencia esté lista para aceptar conexiones.
Usa healthchecks y/o reintentos cliente con backoff sensato.
6) ¿Por qué el servidor escucha en 127.0.0.1 dentro del contenedor?
Muchos servicios por defecto se atan a loopback por “seguridad.” En contenedores, eso a menudo bloquea el único tráfico que realmente quieres: otros contenedores.
Enlaza a 0.0.0.0 y protege con autenticación/ACLs en lugar de esconderte detrás de loopback.
7) ¿Pueden los firewalls causar “conexión rechazada” en Docker?
Sí. Las reglas de rechazo pueden generar RST/ICMP que parecen un rechazo. Más comúnmente, los firewalls causan timeouts al descartar paquetes.
Si tu ruta implica puertos publicados o enrutamiento entre hosts, valida la integración del firewall del host con Docker.
8) ¿Deben los contenedores llamarse entre sí vía el puerto publicado del host?
Usualmente no. Añade NAT, modos de fallo extra y acoplamiento al host. Usa networking servicio-a-servicio vía la red Docker compartida.
Publica puertos para clientes fuera de Docker.
9) ¿Por qué funciona localmente pero falla en CI o en un servidor compartido?
Las configuraciones locales suelen tener menos redes, menos proyectos Compose y menos políticas de firewall. En CI/entornos compartidos,
los nombres de red colisionan, los servicios arrancan en distinto orden y las bases de firewall pueden diferir. Haz las redes explícitas y añade healthchecks.
10) ¿Qué pasa si DNS resuelve correctamente pero aún obtengo rechazado?
Entonces DNS no es tu problema. Rechazado apunta a sockets en escucha, direcciones de bind, puertos equivocados o crashes del proceso servidor.
Ejecuta ss -lntp dentro del contenedor servidor y verifica que el puerto esté escuchando en 0.0.0.0.
Conclusión: pasos prácticos siguientes
“Conexión rechazada” no es un misterio; es un diagnóstico esperando a cerrarse. Tu trabajo es dejar de tratarlo como un fenómeno meteorológico.
Prueba el objetivo, prueba la resolución de nombres, prueba la membresía de red, prueba el listener y sólo entonces discute sobre firewalls.
Pasos siguientes que pagan la factura:
- Audita cada configuración entre servicios buscando
localhost, IPs del host y puertos publicados usados internamente. Reemplaza por nombres de servicio y puertos de contenedor. - Añade healthchecks para servicios stateful y haz que tus clientes manejen el arranque con backoff acotado.
- Haz explícitas las redes de Compose, especialmente cuando múltiples proyectos deban comunicarse.
- Estandariza un runbook corto:
getent,nc,ss,docker network inspect. Hazlo memoria muscular.