Redes en Docker: de ‘no puedo conectar mis contenedores’ a entenderlo de verdad
Pasé casi tres horas intentando que un contenedor de frontend se comunicara con un contenedor de backend. Hacía curl http://localhost:3000 desde dentro del frontend y no llegaba nada. El problema no era el código: era que no entendía cómo funcionan las redes en Docker.
El modelo de red de Docker
Cada contenedor tiene su propia interfaz de red virtual y su propia dirección IP dentro de la red Docker. El localhost dentro de un contenedor es ese contenedor, no el host ni otro contenedor. Ese fue mi error: intentar llegar a otro contenedor como si fuera mi máquina.
Los tres drivers de red nativos
┌─────────────────────────────────────────────────────────────────┐
│ BRIDGE (default) │
│ │
│ Host ──── docker0 ──── contenedor1 (172.17.0.2) │
│ └─── contenedor2 (172.17.0.3) │
│ │
│ • Red virtual privada │
│ • Contenedores aislados entre sí por defecto │
│ • Se exponen puertos explícitamente con -p │
├─────────────────────────────────────────────────────────────────┤
│ HOST │
│ │
│ Host ──── contenedor (comparte la red del host) │
│ │
│ • Sin aislamiento de red │
│ • Máximo rendimiento (sin NAT) │
│ • Solo disponible en Linux │
├─────────────────────────────────────────────────────────────────┤
│ NONE │
│ │
│ contenedor (sin red - solo loopback) │
│ │
│ • Aislamiento total │
│ • Para procesos que no necesitan red │
└─────────────────────────────────────────────────────────────────┘
La solución: redes definidas por el usuario
La red bridge por defecto tiene una limitación importante: los contenedores no se pueden resolver por nombre, solo por IP. Las redes personalizadas solucionan esto con DNS automático. Esta es la forma correcta de conectar contenedores.
# Crear una red personalizada
docker network create mi-app-network
# Levantar backend
docker run -d --name backend --network mi-app-network -e DB_HOST=postgres mi-api:latest
# Levantar frontend - puede resolver "backend" por nombre
docker run -d --name frontend --network mi-app-network -p 80:3000 -e API_URL=http://backend:8080 mi-frontend:latest
# Levantar base de datos en la misma red
docker run -d --name postgres --network mi-app-network -v postgres-data:/var/lib/postgresql/data -e POSTGRES_PASSWORD=secreto postgres:16-alpine
# Ahora desde frontend podés hacer:
# curl http://backend:8080/api/health ✅
# La base de datos NO está expuesta al exterior (sin -p)
Publicación de puertos: qué exponés y qué no
Un error común es publicar todos los puertos de todos los servicios. La buena práctica: solo el punto de entrada de tu aplicación (el frontend o la API pública) se expone al host. La base de datos, el cache, los servicios internos — solo accesibles dentro de la red Docker.
# ❌ Exponer todo - superficie de ataque innecesaria
docker run -p 5432:5432 postgres # DB expuesta al mundo
docker run -p 6379:6379 redis # Cache expuesta al mundo
docker run -p 8080:8080 mi-api # API interna expuesta
# ✅ Solo exponer el punto de entrada
docker run -p 80:80 mi-nginx # Solo el proxy/frontend al exterior
# El resto: en red interna, sin -p
Comandos de diagnóstico de red
# Ver redes existentes
docker network ls
# Inspeccionar una red (ver qué contenedores están conectados)
docker network inspect mi-app-network
# Conectar/desconectar un contenedor de una red en caliente
docker network connect mi-app-network contenedor-existente
docker network disconnect mi-app-network contenedor-existente
# Diagnóstico de conectividad desde dentro de un contenedor
docker exec -it frontend ping backend
docker exec -it frontend curl http://backend:8080/health
docker exec -it frontend nslookup backend # resolución DNS
Lo que debería haber hecho desde el principio
La solución a mis tres horas de frustración era simple: crear una red personalizada y usar los nombres de contenedor como hostnames. Desde que lo entendí, la comunicación entre servicios es trivial. La clave mental: dentro de una red Docker personalizada, el nombre del contenedor es el hostname. http://backend:8080 funciona igual que http://192.168.1.100:8080, pero sin tener que saber IPs que cambian.
← Artículo anterior: Volúmenes y persistencia | Serie Docker Completo | Próximo: Docker Compose →
← Artículo anterior: Cuando perdí datos de producción por no usar volúmenes (y cómo no repetirlo) | Serie Docker Completo | Próximo: Docker Compose: el día que dejé de levantar contenedores a mano →

Dejar un comentario
¿Quieres unirte a la conversación?Siéntete libre de contribuir!