"Connection refused" entre dos contenedores es el error Docker por excelencia, y casi siempre nace de no entender que cada contenedor vive en su propia red con su propio localhost. Este artículo ordena el modelo de redes completo — qué existe, cuándo usarlo y cómo depurarlo cuando falla.
El localhost trampa
Primer choque mental: dentro de un contenedor, localhost apunta al PROPIO contenedor. Tu app en el contenedor A llamando a localhost:5432 busca una base de datos dentro de sí misma — no la encuentra. La BD corre en el contenedor B, que tiene su propia pila de red aislada.
Todo el modelo de redes Docker responde a esa realidad. Las soluciones, de más a menos recomendadas:
Red bridge por defecto (y por qué evitarla)
Sin configurar nada, los contenedores corren en bridge — la red docker0 (172.17.0.0/16). Ahí los contenedores se ven por IP interna pero no por nombre: no hay DNS automático. Comunicarse exige IPs dinámicas que cambian en cada recreación... frágil hasta el absurdo.
Es la red correcta para un contenedor suelto (docker run puntual). Para stacks multi-contenedor, no.
Redes custom: DNS integrado gratis
Crear una red definida por usuario activa el servidor DNS embebido de Docker (127.0.0.11): cada contenedor resuelve los demás por nombre de contenedor:
docker network create miapp-net
docker run -d --name db --network miapp-net postgres:16-alpine
docker run -d --name app --network miapp-net -e DATABASE_URL=postgres://app:x@db:5432/app miapp
Ese hostname db en la cadena de conexión es el DNS interno resolviendo el nombre del contenedor. Con Docker Compose esto es automático: todos los servicios de un compose comparten red del proyecto y se descubren por nombre de servicio — exactamente el patrón del stack app + PostgreSQL.
Alias adicionales para flexibilidad:
services:
db:
networks:
miapp-net:
aliases: [postgres, database]
Publicar puertos vs exponerlos
Dos conceptos distintos que conviene no mezclar:
EXPOSE (en Dockerfile): documentación pura — declara qué puerto usa el proceso. No abre nada al exterior.
-p / ports (al ejecutar): publica un puerto del HOST redirigido al contenedor. -p 8080:80 = tu máquina en 8080 → contenedor 80.
Reglas prácticas que evitan sustos:
- Publica solo lo que debe ser alcanzable desde fuera del host (normalmente solo el reverse proxy o el propio frontend).
- Base de datos y servicios internos: sin publicar. Se acceden por la red interna.
- Si necesitas acceso local de emergencia a un servicio interno, publica atado a loopback:
-p 127.0.0.1:5432:5432. Nunca0.0.0.0para algo privado. - Dos contenedores publicando el mismo puerto host = conflicto de arranque.
Modo host: cuando el contenedor ES la máquina
--network host elimina el aislamiento: el proceso corre con la pila de red del host directamente. Sin mapeos de puertos (los escuchados son reales), rendimiento máximo de red, y también máximo riesgo: nada separa al contenedor del sistema.
Casos donde tiene sentido: proxies de altísimo rendimiento, herramientas de monitoreo que deben ver todo el tráfico. Fuera de Linux no funciona igual (Docker Desktop lo simula); úsalo como excepción justificada, nunca por comodidad.
Redes entre proyectos
Por defecto, redes son por-proyecto en Compose (proyecto_default). ¿Un Traefik global proxyando varios proyectos? Red externa compartida:
networks:
proxy-net:
external: true
services:
web:
networks: [proxy-net]
docker network create proxy-net
Todos los proyectos que se enganchen a proxy-net quedan alcanzables por Traefik sin exponer puertos individuales — el patrón estándar de self-hosting con Nginx/Traefik como puerta única.
Diagnóstico cuando falla
Kit mínimo, en orden de sospecha:
# 1. ¿Están en la MISMA red?
docker network inspect miapp-net --format '{{range .Containers}}{{.Name}} {{end}}'
# 2. ¿Resuelve el nombre?
docker exec app ping db # o: getent hosts db
# 3. ¿Escucha el destino en 0.0.0.0 (no solo 127.0.0.1)?
docker exec db ss -tlnp
# 4. Ruta completa de paquetes entre contenedores
docker exec app nc -zv db 5432
Los tres fallos más frecuentes en la práctica: contenedores en redes distintas (cada docker run sin --network creó la suya), servicio escuchando en localhost interno en vez de 0.0.0.0, y typo en el nombre de servicio. En ese orden.
Preguntas frecuentes
¿Contenedor → máquina host? Desde Linux, IP especial host.docker.internal requiere flag --add-host=host.docker.internal:host-gateway; en Mac/Windows viene de serie. Útil para alcanzar una BD instalada nativamente durante migraciones.
¿Las redes Docker son seguras? El aislamiento por red es real pero NO sustituye autenticación: cualquier contenedor comprometido en tu red puede hablar con los demás. Minimiza redes compartidas entre proyectos no confiables.
¿IPv6? Docker soporta redes IPv6 habilitándolas explícitamente por red; sigue siendo terreno de configuración avanzada salvo que lo necesites específicamente.
Monta stacks multi-servicio con nombres y dependencias correctas usando nuestro generador de Docker Compose, gratis y sin registro.