Redes de Docker Compose: DNS, host y puertos publicados
Entiende la red bridge predeterminada, el DNS por nombre de servicio, cuándo usar host, cómo compartir redes y por qué un puerto publicado puede saltarse UFW.
Lo que Compose crea antes de iniciar la aplicación
La red de Docker Compose comienza con una regla: docker compose up crea una red privada para el proyecto, conecta todos los servicios a ella y permite que esos servicios se comuniquen entre sí mediante el nombre del servicio. No es necesario escribir una sola línea de networks: para obtener ese comportamiento. La mayoría de las confusiones sobre las redes de Compose se deben a no saber que esta configuración predeterminada ya existe.
Este es un archivo pequeño. Guárdalo como compose.yaml en un directorio llamado shop.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: exampleInicia el proyecto y revisa lo que creó Docker:
docker compose up -d
docker network lsLa lista ahora contiene una red llamada shop_default. Compose la nombra <project>_default, y el nombre del proyecto usa de forma predeterminada el nombre del directorio en minúsculas. Puedes sobrescribirlo con docker compose -p myproject up -d o mediante un name: myproject de nivel superior en el archivo. Su controlador es bridge, que es un switch virtual dentro del host. Cada contenedor obtiene una dirección en una subred privada, y el tráfico saliente se traduce a la dirección del host al salir.
docker compose down elimina esa red. Por eso un contenedor antiguo de un proyecto anterior puede mantener una red abierta: Docker responde con error while removing network: network shop_default has active endpoints, y la solución consiste en detener o eliminar el contenedor que todavía está conectado a ella.
Si Compose es nuevo para ti, conviene leer primero Estructura del archivo de Compose y comandos del ciclo de vida, porque todo lo siguiente presupone que puedes iniciar y detener un proyecto.
DNS por nombre de servicio es la parte que los principiantes suelen pasar por alto
En cualquier red definida por el usuario, Docker ejecuta un servidor DNS integrado que cada contenedor ve en 127.0.0.11. Resuelve los nombres de servicio en las direcciones actuales de los contenedores. Por tanto, web llega a la base de datos mediante el nombre de host db, en el puerto 5432, sin ninguna configuración.
docker compose exec web getent hosts dbEsto imprime una línea similar a 172.18.0.2 db. Si no imprime nada, los dos servicios no están en la misma red.
El error que casi todos cometen alguna vez es usar localhost en la configuración de la aplicación. Dentro de un contenedor, localhost identifica ese contenedor, no el host ni el otro servicio. Los clientes de Postgres lo indican claramente:
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP connections on port 5432?La cadena de conexión debe ser postgresql://postgres:example@db:5432/postgres. La parte del host es el nombre del servicio.
Hay dos detalles que ahorran tiempo más adelante. Los nombres se resuelven según lo que se está ejecutando en ese momento, por lo que docker compose up -d --scale web=3 proporciona un nombre con tres direcciones, y un cliente que almacena el DNS en caché indefinidamente se quedará asociado a un contenedor detenido. Además, la red heredada bridge, que usa un docker run simple sin --network, no ofrece resolución de nombres. Por eso las indicaciones sobre enlaces entre contenedores de 2016 no coinciden con lo que se observa.
No necesita ports: para conectar dos servicios
ports: publica un puerto del contenedor en el host. Se usa para el tráfico que llega desde fuera de Docker. No tiene ninguna relación con el tráfico entre servicios, que ya funciona en todo el rango de puertos de la red del proyecto.
Por tanto, ports: - "5432:5432", que muchas personas agregan a su servicio de base de datos, no aporta ningún beneficio y causa un problema real: expone Postgres en la interfaz pública del servidor. Elimínelo. Si necesita acceder al servicio desde su portátil para realizar una migración, vincúlelo a loopback con "127.0.0.1:5432:5432" y acceda mediante un túnel SSH. La diferencia entre un socket en escucha, un puerto publicado y una regla de firewall se explica en cómo funcionan los puertos y los servicios en escucha en Linux.
expose: solo sirve como documentación en Compose. No abre nada, porque no había nada cerrado entre los contenedores de la misma red.
Cuándo es adecuado usar network_mode host y qué costes implica
El modo host elimina el espacio de nombres de red propio del contenedor y permite que el proceso use directamente las interfaces del host.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinityHay razones concretas para usarlo. Un proceso que necesita ver tráfico de difusión o multidifusión en la red local, como la detección de dispositivos para un servidor multimedia o un hub de automatización doméstica, no puede verlo detrás de un bridge, porque el bridge no reenvía ese tráfico al contenedor. Un agente de monitorización que lee los contadores de las interfaces del host necesita acceder a esas interfaces. Además, se omite el salto de traducción de direcciones, lo que puede ser importante con tasas altas de paquetes.
Los costes son específicos.
ports: deja de funcionar. Docker advierte que los puertos publicados se descartan al usar el modo de red host, y el contenedor enlaza los puertos que enlace su proceso. Dos contenedores en modo host que intenten usar el puerto 8080 entran en conflicto y el segundo termina con bind: address already in use.
La resolución de nombres mediante el nombre del servicio deja de funcionar en ambas direcciones. El contenedor no está en la red del proyecto, por lo que no puede resolver db, y los otros servicios tampoco pueden resolverlo. Solo puede acceder a ellos mediante los puertos publicados en el host, normalmente en 127.0.0.1.
El aislamiento desaparece. Un proceso que enlace 0.0.0.0 dentro de un contenedor en modo host escucha en todas las interfaces del servidor, incluida la interfaz pública, exactamente igual que un paquete instalado con apt. Esto tiene una ventaja: este tráfico sigue la ruta de entrada normal, por lo que las reglas de UFW sí se aplican, algo que no ocurre con los puertos publicados.
El modo host es una función de Linux Docker Engine. Docker Desktop solo lo admite desde la versión 4.34 y después de activarlo. Además, los contenedores no pueden enlazar direcciones IP del host y solo se gestionan TCP y UDP. Si parte del equipo usa servidores Linux y otra parte usa Docker Desktop, el mismo archivo puede comportarse de forma diferente.
Use el modo host cuando necesite las interfaces del host. No lo use para resolver un problema de conexión, porque normalmente sustituye un problema por otro más difícil.
Conectar dos proyectos de Compose con una red externa
Una red creada por un proyecto no es visible para otro. Por eso un proxy inverso en proxy/compose.yaml no puede ver una aplicación en app/compose.yaml, aunque estén en el mismo servidor. La solución es una red que no pertenezca a ninguno de los dos proyectos.
Créala una vez, manualmente:
docker network create edgeDespués, declárala como externa en cada proyecto. En el proxy:
services:
proxy:
image: traefik:v3.1
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- edge
networks:
edge:
name: edge
external: trueEn la aplicación:
services:
app:
image: nginx:1.27
networks:
- edge
- internal
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
networks:
- internal
networks:
edge:
name: edge
external: true
internal:external: true indica a Compose que se conecte a una red existente en lugar de crear una nueva, y que la deje en su lugar en docker compose down. La clave independiente name: es más importante de lo que parece: sin ella, Compose busca una red cuyo nombre sea exactamente edge. Con ella, puedes asignar un nombre a la red en el archivo y otro distinto en el host.
Si la red no existe, Compose se niega a iniciar e indica que la red se declaró como externa, pero no se encontró. Créala primero.
Observa qué hace el archivo de la aplicación con internal. La base de datos solo está en esa red local del proyecto, por lo que el proxy no puede acceder a ella y solo app puede hacerlo. Añadir internal: true en una red va más allá y elimina por completo su ruta hacia el exterior. Es un buen valor predeterminado para una base de datos, pero debes conocer un coste antes de configurarlo: un contenedor conectado a una red interna no puede descargar nada, por lo que un entrypoint que ejecute apt-get update o pip install al iniciar se bloqueará y después fallará por tiempo de espera.
Para consultar una configuración completa con reglas de enrutamiento y certificados, consulta ejecutar varias aplicaciones detrás de una instancia de Traefik.
Los puertos publicados omiten UFW
Esta es la parte de las redes de Compose que puede convertirse en un incidente de seguridad. Publicas un puerto, compruebas que UFW está activo y deniega todo excepto SSH, pero el servicio sigue siendo accesible desde Internet.
sudo ufw status
curl http://203.0.113.10:8080UFW indica que el puerto está bloqueado. El curl devuelve la página de todos modos. No hay ningún fallo. Docker escribe directamente sus propias reglas de traducción de direcciones y reenvío en iptables. El tráfico dirigido a un puerto publicado del contenedor se reenvía al contenedor en lugar de entregarse al host, por lo que nunca pasa por la cadena que UFW administra para el tráfico destinado localmente. Además, las reglas de Docker se evalúan antes que las de UFW.
La solución rápida es publicar el puerto solo donde sea necesario:
ports:
- "127.0.0.1:8080:80"Esto vincula el lado del host a loopback. El puerto queda accesible desde el propio servidor y mediante un túnel SSH, pero desde ningún otro lugar. Coloca el punto de entrada público detrás de un proxy inverso que publique 80 y 443 de forma intencionada. La explicación completa, incluida la cadena DOCKER-USER para los casos en los que debes filtrar un puerto publicado, está en por qué Docker publica directamente por fuera de UFW y cómo corregirlo.
Cómo depurarlo con cuatro comandos
Empiece comprobando a qué red está conectado realmente cada contenedor:
docker network inspect shop_defaultEl bloque Containers muestra todos los contenedores conectados y sus direcciones. Si un servicio no aparece en esa lista, está en otra red, usa el modo de red del host o no está en ejecución.
Pruebe la resolución de nombres desde un contenedor temporal conectado a la misma red. Así no necesita herramientas dentro de sus propias imágenes:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432Si nslookup falla, el problema está en la resolución de nombres o en la pertenencia a la red. Si nslookup funciona pero nc falla, el servicio está en ejecución, pero no escucha en ese puerto o escucha en 127.0.0.1 dentro de su propio contenedor en lugar de 0.0.0.0. Esto es habitual en los servidores de desarrollo. La corrección está en la dirección de enlace de la aplicación, no en Docker.
Hay otro fallo que parece un error de Docker. Si los contenedores pueden comunicarse entre sí, pero no pueden acceder a una máquina de la red de la oficina o de la VPN, es probable que la subred de Docker se superponga con esa red. Docker asigna direcciones desde 172.17.0.0/16 de forma predeterminada. Cambie el rango en /etc/docker/daemon.json:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}Después, ejecute sudo systemctl restart docker y vuelva a crear las redes afectadas, porque una red existente conserva la subred con la que se creó.
FAQ
¿Por qué mis contenedores no pueden comunicarse mediante el nombre del servicio?
No están en la misma red. Compose coloca automáticamente cada servicio en <project>_default, pero en cuanto se añade una lista networks: a un servicio, esa lista se convierte en el conjunto completo de redes del servicio y la red predeterminada deja de estar implícita. Ejecute docker network inspect <network> y compruebe que ambos contenedores aparecen en el bloque Containers. Compruebe también que ninguno de los servicios usa network_mode: host, porque un contenedor en modo host no está conectado a ninguna red de Docker y no puede resolver nombres de servicio.
¿Necesito publicar puertos para que un servicio se comunique con otro?
No. En una red de Compose, los demás contenedores de esa red pueden acceder a todos los puertos de cada contenedor. ports: solo sirve para exponer un contenedor al tráfico externo a Docker, y expose: es documentación. Publicar el puerto de una base de datos es una práctica habitual y costosa, porque coloca la base de datos en la interfaz pública del servidor.
¿Cuál es la diferencia entre las redes bridge y host?
bridge proporciona al contenedor su propio espacio de nombres de red y una dirección en un switch virtual, con resolución automática de nombres entre contenedores y traducción del tráfico saliente. host proporciona directamente al contenedor la pila de red del host: no hay una dirección independiente, no hay resolución mediante el nombre del servicio, no se publican puertos y no existe aislamiento respecto a los demás procesos que escuchan en el host. bridge es la opción predeterminada y la adecuada, salvo que el proceso necesite las interfaces del host.
¿Cómo conecto contenedores de dos archivos de Compose diferentes?
Cree una red compartida con docker network create edge. Después, declárela en ambos archivos con external: true y conecte los servicios que necesiten comunicarse. Compose no la creará ni la eliminará. Si omite el paso de creación, Compose se niega a iniciar e indica que la red está declarada como externa, pero no se ha encontrado.
¿Por qué se puede acceder a mi contenedor desde Internet si UFW bloquea el puerto?
Porque un puerto publicado se gestiona mediante las reglas de reenvío que Docker añade a iptables. Esas reglas se procesan antes que las de UFW, y el tráfico reenviado tampoco pasa por la cadena que filtra UFW. Enlace el lado del host a loopback con "127.0.0.1:8080:80" y coloque todo lo público detrás de un proxy inverso en los puertos 80 y 443.