Guía Práctica sobre Redes Docker Personalizadas y Comunicación entre Contenedores
Esta guía ofrece una exploración práctica de las redes puente personalizadas de Docker y su papel en la comunicación entre contenedores. Aprenda a crear, gestionar y conectar contenedores usando la CLI de Docker y Docker Compose. Descubra cómo las redes personalizadas permiten la resolución automática de DNS, mejoran el aislamiento y simplifican la comunicación entre servicios, lo que conduce a aplicaciones contenerizadas más robustas y escalables.
Guía Práctica sobre Redes Docker Personalizadas y Comunicación entre Contenedores
Las redes Docker personalizadas son una de esas características que parecen opcionales hasta que ejecutas más de un contenedor. El puente predeterminado puede servir para una prueba rápida, pero un puente definido por el usuario te proporciona nombres de servicio predecibles, un aislamiento más limpio y una depuración más sencilla. Para una aplicación pequeña con un contenedor web, un contenedor de API y una base de datos, esa diferencia es inmediata: la API puede conectarse a db:5432 en lugar de perseguir la IP que Docker haya asignado hoy.
Esta guía se centra en las redes puente definidas por el usuario en un único host Docker. Las redes superpuestas, la red de Kubernetes y el descubrimiento de servicios de Swarm resuelven problemas relacionados en configuraciones de múltiples hosts, pero la red puente sigue siendo la herramienta diaria para el desarrollo local, despliegues pequeños y proyectos de Docker Compose.
Por qué el puente predeterminado se vuelve incómodo
Docker crea una red llamada bridge automáticamente. Si ejecutas contenedores sin especificar una red, normalmente terminan ahí. Funciona para casos simples, pero no es agradable para aplicaciones con múltiples contenedores.
En una red puente definida por el usuario, Docker proporciona DNS integrado para nombres de contenedor y nombres de servicio de Compose. En el puente predeterminado, el descubrimiento basado en nombres es limitado y la vinculación heredada no es un patrón con el que debas construir. El resultado práctico es que las redes personalizadas te permiten configurar aplicaciones con nombres de host estables:
DATABASE_HOST=db
REDIS_HOST=redis
API_BASE_URL=http://api:3000
Eso es más fácil de leer, más fácil de mover entre máquinas y menos frágil que las direcciones IP de los contenedores.
Las redes personalizadas también crean un límite más claro. Los contenedores conectados a la misma red pueden comunicarse entre sí. Los contenedores en redes diferentes no pueden, a menos que conectes un contenedor a ambas o publiques puertos a través del host. Esto no es un modelo de seguridad completo, pero es una capa útil de separación.
Crear una red con la CLI de Docker
Crea un puente definido por el usuario:
docker network create app-net
Inicia dos contenedores en ella:
docker run -d --name db --network app-net -e POSTGRES_PASSWORD=devpass postgres:16
docker run -d --name adminer --network app-net -p 8080:8080 adminer
Desde el contenedor adminer, el nombre de host de la base de datos es db. No necesitas conocer su IP.
Inspecciona la red:
docker network inspect app-net
Verás el controlador, la subred, la puerta de enlace y los contenedores conectados. Al depurar, este comando responde una pregunta básica: ¿están los dos contenedores realmente en la misma red?
Puedes conectar un contenedor existente:
docker network connect app-net some-container
Y desconectarlo:
docker network disconnect app-net some-container
Docker no eliminará una red mientras haya contenedores conectados. Desconecta o elimina los contenedores primero:
docker network rm app-net
Los puertos publicados son diferentes de los puertos de contenedor a contenedor
Una confusión común: los contenedores en la misma red Docker no necesitan puertos de host publicados para comunicarse entre sí. Los puertos publicados son para el tráfico que entra desde el host o desde fuera del host.
Si un contenedor de API escucha en el puerto 3000 y un contenedor web está en la misma red, el contenedor web puede llamar:
http://api:3000
Solo necesitas -p 3000:3000 si quieres acceder a la API desde el navegador de tu portátil o desde otro host a través del host Docker.
Esto significa que tu base de datos normalmente no debería publicar un puerto de host en una configuración similar a producción a menos que algo fuera de Docker necesite acceso directo. Deja que la API acceda a db:5432 a través de la red Docker privada.
Usa Compose para aplicaciones normales de múltiples servicios
Docker Compose crea una red predeterminada para el proyecto incluso si no defines una. Los servicios pueden comunicarse entre sí por nombre de servicio:
services:
web:
image: nginx:latest
ports:
- "8080:80"
depends_on:
- api
api:
image: my-api:latest
environment:
DATABASE_HOST: db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: devpass
En ese archivo, api puede acceder a db usando el nombre de host db. web puede acceder a api usando el nombre de host api, asumiendo que la configuración a nivel de aplicación apunta allí.
También puedes definir redes con nombre cuando quieras una intención o separación más clara:
services:
web:
image: nginx:latest
ports:
- "8080:80"
networks:
- frontend
api:
image: my-api:latest
environment:
DATABASE_HOST: db
networks:
- frontend
- backend
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: devpass
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
Aquí, web no puede hablar directamente con db porque no comparten una red. api es el puente entre las dos capas de la aplicación. Esta es una forma útil para servicios reales: exponer solo el servicio de borde al host, mantener la base de datos privada y conectar cada servicio solo donde necesita comunicarse.
depends_on no es disponibilidad
depends_on de Compose controla el orden de inicio en el uso común de Compose, pero no garantiza que la base de datos esté lista para aceptar conexiones. Tu API puede iniciarse después de que el proceso del contenedor db se inicie y aún así fallar porque PostgreSQL se está inicializando.
Maneja la disponibilidad en la aplicación con reintentos, o usa un health check y una configuración de Compose que respete la salud del servicio para tu versión de Compose y flujo de trabajo. Incluso entonces, la lógica de reintento a nivel de aplicación sigue siendo el hábito más confiable porque las bases de datos pueden reiniciarse después del inicio inicial.
Una configuración práctica de API usa DATABASE_HOST=db y reintenta la conexión durante un breve período antes de salir con un error claro.
Las subredes personalizadas son útiles, pero no las uses en exceso
Puedes elegir una subred:
docker network create --subnet 172.28.0.0/16 app-net
En Compose:
networks:
backend:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
Esto ayuda cuando la subred automática de Docker se superpone con una VPN, red de oficina u otra ruta en el host. No es necesario para la mayoría de los proyectos. Codificar IPs de contenedores debería ser raro; los nombres de servicio suelen ser el contrato mejor.
Solucionar problemas de comunicación de red
Cuando un contenedor no puede alcanzar a otro, verifica en este orden:
- ¿Ambos contenedores están en ejecución?
- ¿Están conectados a la misma red?
- ¿El cliente está usando el nombre del servicio/contenedor, no
localhost? - ¿El servidor está escuchando en el puerto e interfaz esperados?
- ¿El puerto está publicado solo cuando se necesita acceso desde el host?
El error de localhost es especialmente común. Dentro de un contenedor, localhost significa ese mismo contenedor, no el host Docker ni otro servicio. Si la API intenta conectarse a localhost:5432, está buscando PostgreSQL dentro del contenedor de la API. Usa db:5432 cuando el servicio de base de datos se llame db.
Inspecciona las redes:
docker network inspect app-net
Ejecuta un contenedor de diagnóstico temporal en la misma red:
docker run --rm -it --network app-net alpine sh
Dentro, instala o usa las herramientas disponibles según sea necesario:
getent hosts db
nc -vz db 5432
Las imágenes mínimas pueden no tener nc, curl o herramientas DNS instaladas. Un contenedor de depuración de corta duración suele ser más limpio que agregar paquetes de solución de problemas a tu imagen de aplicación.
Un patrón predeterminado sensato
Para la mayoría de las aplicaciones de un solo host, usa Compose y deja que cree la red del proyecto. Agrega redes explícitas cuando necesites separación, como frontend y backend. Usa nombres de servicio para el tráfico interno. Publica solo los puertos que los humanos, proxies inversos o sistemas externos necesiten alcanzar.
Eso te da una configuración que es fácil de explicar:
- El navegador accede a
localhost:8080porquewebpublica un puerto. webaccede aapia través de la red Docker.apiaccede adba través de la red backend.dbno tiene puerto de host a menos que haya una razón operativa real.
Las redes Docker personalizadas no son solo una característica interesante. Son la diferencia entre contenedores que simplemente se ejecutan en la misma máquina y servicios que tienen un modelo de comunicación claro.
Los alias de red pueden facilitar las migraciones
A veces una aplicación espera un nombre de host que no quieres usar como nombre de servicio de Compose. Puedes agregar un alias en una red:
services:
postgres:
image: postgres:16
networks:
backend:
aliases:
- database
networks:
backend:
driver: bridge
Los contenedores en backend ahora pueden acceder al servicio como postgres o database. Esto es útil al migrar una aplicación más antigua que ya usa DATABASE_HOST=database, pero no usaría alias en todas partes. Los nombres de servicio son más simples cuando controlas la configuración de la aplicación.
El acceso al host es un problema separado
Un contenedor que se comunica con otro contenedor es diferente de un contenedor que se comunica con el host Docker. En Docker Desktop, host.docker.internal está comúnmente disponible. En Linux, el soporte depende de la versión y configuración de Docker; muchos equipos lo agregan explícitamente cuando es necesario:
docker run --add-host=host.docker.internal:host-gateway ...
Úsalo con moderación. Si un contenedor depende en gran medida de servicios que se ejecutan directamente en el host, tu configuración puede volverse más difícil de reproducir en CI o en la máquina de otro desarrollador. Para bases de datos y cachés, ejecutar la dependencia como otro servicio en la misma red Docker suele ser más limpio.
Los puertos internos deben coincidir con el proceso, no con el comentario del Dockerfile
La red Docker no se preocupa por lo que dice una línea EXPOSE del Dockerfile a menos que las herramientas lo usen como metadatos. La aplicación debe escuchar realmente en el puerto al que llamas. Si una aplicación Node escucha en el 3000, otros contenedores deben usar api:3000 incluso si alguien escribió EXPOSE 8080 por error.
También verifica la dirección de enlace. Un servicio que escucha en 127.0.0.1 dentro de su contenedor puede no ser accesible desde otros contenedores. Para el tráfico de contenedor a contenedor, el proceso generalmente necesita escuchar en 0.0.0.0 o en la interfaz de red del contenedor.
Mantén el diseño de red aburrido
Es tentador crear muchas redes porque la función existe. Comienza con las rutas de comunicación que realmente necesitas. Una aplicación pequeña podría necesitar solo la red predeterminada de Compose. Una aplicación web más realista podría necesitar frontend y backend. Más allá de eso, cada nueva red debe tener una razón que alguien pueda explicar durante un incidente.
Un buen diseño de red facilita la solución de problemas. Cuando web no puede acceder a db, y sabes que intencionalmente no comparten una red, la respuesta es arquitectónica en lugar de misteriosa. Cuando cada servicio está conectado a cada red, la red ya no documenta nada.
Una revisión del mundo real antes de enviar
Antes de dar por terminado un script o configuración de contenedor, léelo una vez como si fueras la próxima persona que tiene que depurarlo a las 2 a.m. Eso cambia lo que notas. Un mensaje que tenía sentido mientras escribías el script puede ser ambiguo cuando aparece en un registro de CI. Un nombre de servicio Docker que parecía obvio puede no coincidir con el nombre de variable en la aplicación. Un valor predeterminado de Bash puede ser seguro para desarrollo y peligroso para producción.
Me gusta hacer una prueba en seco corta con valores deliberadamente incómodos. Usa una ruta con espacios. Usa un valor opcional vacío. Prueba un nombre de archivo que comience con un guión. Ejecuta el script desde un directorio de trabajo diferente. Inicia el contenedor sin una variable de entorno esperada. Estas pruebas no son sofisticadas, pero detectan las suposiciones que generalmente se rompen primero.
También verifica el mensaje de fallo. Si la única salida es falló, el consejo del artículo no ha llegado a la implementación. Un fallo útil dice qué valor se usó, qué verificación falló y qué puede cambiar el operador. Eso no significa volcar cada variable de entorno o imprimir secretos. Significa ser específico donde la especificidad ayuda: la ruta de configuración, el nombre del comando faltante, el nombre de la red, el nombre de host del servicio o el puerto que el proceso intentó enlazar.
El hábito final es mantener los ejemplos cerca de la forma en que el sistema se ejecuta realmente. Si la producción usa Compose, prueba con Compose. Si un script es lanzado por systemd, pruébalo con systemd o con un entorno igualmente mínimo. Si se supone que un comando es seguro para copiar y pegar, incluye las comillas, los separadores -- y la validación en el propio ejemplo. Los lectores copian patrones funcionales más a menudo de lo que copian advertencias.
Esa revisión no es burocracia. Es cómo la pequeña automatización se mantiene aburrida. Aburrido es lo que quieres de los mensajes de shell, los cargadores de configuración, la expansión de variables, el diagnóstico de contenedores y la red Docker. Cuanto menos sorprendente sea el comportamiento, más fácil será para el próximo operador confiar en él.
Para la red Docker, documenta la ruta de tráfico prevista junto al archivo Compose o en el README del servicio. Una nota corta como web -> api:3000 -> db:5432 evita mucha confusión. También facilita las revisiones: si alguien publica el puerto de la base de datos o conecta web a la red backend, el cambio tiene que justificarse frente a la ruta prevista.
Cuando la aplicación crezca, revisa el mapa de red. Los alias antiguos, los puertos publicados no utilizados y los servicios conectados a redes que ya no necesitan son fuentes silenciosas de riesgo operativo.