Redes en Docker Compose: DNS, host y puertos
Aprenda cómo funciona la red bridge predeterminada, el DNS por nombre de servicio, cuándo usar host, redes entre proyectos y el puerto publicado que evita 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 networks: para conseguirlo. Gran parte de la confusión sobre las redes de Compose se debe a no saber que esta red predeterminada ya existe.
Este es un archivo pequeño. Guárdelo como compose.yaml en un directorio llamado shop.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: exampleInícielo y compruebe lo que ha creado Docker:
docker compose up -d
docker network lsLa lista ahora contiene una red llamada shop_default. Compose la denomina <project>_default y el nombre del proyecto toma de forma predeterminada el nombre del directorio en minúsculas. Puede 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 conmutador 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 vuelve a eliminar esa red. Por eso un contenedor obsoleto de un proyecto antiguo puede mantener una red abierta: Docker devuelve 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 no conoce Compose, conviene leer primero la estructura del archivo de Compose y los comandos del ciclo de vida, porque todo lo siguiente presupone que puede iniciar y detener un proyecto.
El 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 se conecta a la base de datos mediante el nombre de host db y el puerto 5432, sin ninguna configuración adicional.
docker compose exec web getent hosts dbEsto muestra una línea similar a 172.18.0.2 db. Si no muestra nada, los dos servicios no están en la misma red.
El error que casi todo el mundo comete una vez es usar localhost en la configuración de la aplicación. Dentro de un contenedor, localhost corresponde a ese contenedor, no al host ni al 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 esté ejecutándose en ese momento, por lo que docker compose up -d --scale web=3 devuelve un nombre con tres direcciones, y un cliente que almacene el DNS en caché para siempre se quedará fijado a un contenedor detenido. Además, la red heredada bridge que usa un docker run simple sin --network no tiene resolución de nombres. Por eso las instrucciones sobre enlaces entre contenedores de 2016 no coinciden con lo que observa.
No necesita ports: para conectar dos servicios
ports: publica un puerto del contenedor en el host. Se utiliza para el tráfico que llega desde fuera de Docker. No tiene 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 añaden 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 desde su portátil para realizar una migración, vincúlelo a la interfaz de 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.
En Compose, expose: sólo sirve como documentación. 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 tiene
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 motivos válidos para usarlo. Un proceso que necesita ver tráfico broadcast o multicast de la red local, como el descubrimiento de dispositivos de un servidor multimedia o un hub de domótica, 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 concretos.
ports: deja de funcionar. Docker advierte que los puertos publicados se descartan cuando se usa el modo de red host, y el contenedor enlaza los puertos que enlace su proceso. Si dos contenedores en modo host intentan usar el puerto 8080, entran en conflicto y el segundo termina con bind: address already in use.
La resolución por nombre de servicio desaparece en ambas direcciones. El contenedor no está en la red del proyecto, por lo que no puede resolver db, y los demás servicios tampoco pueden resolverlo. Sólo 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 sólo 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 sólo se gestionan TCP y UDP. Si la mitad del equipo usa servidores Linux y la otra mitad Docker Desktop, el mismo archivo puede comportarse de forma diferente.
Use el modo host cuando necesite las interfaces del host. No lo use para solucionar un problema de conexión, porque normalmente sustituye un problema por otro más difícil.
Conectar dos proyectos de Compose mediante 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 usar una red que no pertenezca a ninguno de los dos proyectos.
Créela una sola vez, manualmente:
docker network create edgeDespués, declárela como externa en cada proyecto. En el lado del 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 el lado de 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 mantenga 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, puede asignar un nombre a la red en el archivo y otro 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éela primero.
Observe qué hace el archivo de la aplicación con internal. La base de datos sólo está conectada a esa red local del proyecto, por lo que el proxy no puede acceder a ella y sólo 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 debe conocer un coste antes de aplicarlo: 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 quedará bloqueado y después fallará por timeout.
Para consultar una configuración completa con reglas de enrutamiento y certificados, vea ejecutar varias aplicaciones detrás de una instancia de Traefik.
Los puertos publicados omiten UFW
Esta es la parte de la red de Compose que puede convertirse en un incidente de seguridad. Publica un puerto, comprueba 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. Sin embargo, curl devuelve la página. No hay ningún fallo. Docker escribe sus propias reglas de traducción de direcciones y reenvío directamente en iptables. El tráfico dirigido a un puerto publicado del contenedor se reenvía al contenedor en lugar de entregarse al host. Por eso 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 breve es publicar sólo 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. Coloque el punto de entrada público detrás de un reverse proxy que publique 80 y 443 de forma intencionada. La explicación completa, incluida la cadena DOCKER-USER para los casos en los que deba filtrar un puerto publicado, está en por qué Docker publica directamente sin pasar por UFW y cómo solucionarlo.
Cómo depurarlo con cuatro comandos
Empiece comprobando en qué red está realmente cada contenedor:
docker network inspect shop_defaultEl bloque Containers muestra todos los contenedores conectados y sus direcciones. Un servicio que no aparece en esa lista está en otra red, usa el modo 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 y 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 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 se comunican entre sí, pero no pueden llegar a un equipo de su red de oficina o VPN, es probable que la subred de Docker se solape con esa red. Docker asigna direcciones desde 172.17.0.0/16 de forma predeterminada. Cambie el pool en /etc/docker/daemon.json:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}A continuación, 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 entre sí mediante el nombre del servicio?
No están en la misma red. Compose coloca automáticamente cada servicio en <project>_default, pero cuando 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 use 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: sólo existe para exponer un contenedor al tráfico externo a Docker, y expose: sirve como 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 tiene una dirección independiente, no permite resolver nombres de servicio, no admite publicación de puertos y no ofrece 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 distintos?
Cree una red compartida con docker network create edge. Después, declárela en ambos archivos mediante external: true y conecte a ella 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 informa de 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 las reglas de reenvío que Docker añade a iptables gestionan los puertos publicados. 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 mediante "127.0.0.1:8080:80" y coloque todo lo público detrás de un reverse proxy en los puertos 80 y 443.