Llevas cuatro lecciones con la misma avería y ya sabes exactamente qué la causa: nslookup aurora-db devuelve NXDOMAIN porque la red bridge por defecto no tiene DNS interno entre contenedores. La conectividad está ahí —nc -zv 172.17.0.2 5432 responde succeeded—, pero la API no sabe cómo se llama su base de datos.
Esta lección es la que lo arregla, y con dos comandos. Pero antes de escribirlos vas a entender de verdad el modelo de red de Docker: por qué cada contenedor tiene su propio localhost, qué hace cada uno de los drivers integrados, en qué se diferencia la bridge por defecto de una red creada por ti, y cuál es la diferencia —que se confunde constantemente— entre publicar un puerto y que dos contenedores se hablen. Después crearás aurora-net, moverás los tres servicios dentro, y ejecutarás el curl que llevas todo el curso esperando. Y para rematar, pondrás delante aurora-web con Nginx como proxy inverso, y abrirás la librería completa en el navegador.
Contenido
- Por qué los contenedores no se ven entre sí
- Los drivers de red integrados
- La bridge por defecto frente a las redes definidas por el usuario
- Los comandos de
docker network - Publicación de puertos, revisitada
- Práctica:
aurora-nety la resolución de nombres - El momento de la verdad:
/librosdevuelve los ocho libros aurora-web: Nginx como proxy inverso- Conectar y desconectar redes en caliente
- Buenas prácticas de red
- Por qué los contenedores no se ven entre sí
Un contenedor no es solo un proceso aislado: tiene su propio namespace de red. Eso significa que posee, en exclusiva, su lista de interfaces, su tabla de rutas, sus reglas de cortafuegos, sus puertos y —esto es lo que confunde a todo el mundo— su propio localhost.
flowchart TB
subgraph HOST["Host (tu máquina)"]
direction TB
HL["localhost del host<br/>127.0.0.1"]
subgraph C1["Contenedor aurora-api"]
L1["SU localhost<br/>127.0.0.1"]
E1["eth0: 172.17.0.4"]
P1["node escuchando en :3000"]
end
subgraph C2["Contenedor aurora-db"]
L2["SU localhost<br/>127.0.0.1"]
E2["eth0: 172.17.0.2"]
P2["postgres escuchando en :5432"]
end
BR["docker0<br/>bridge 172.17.0.1"]
E1 --- BR
E2 --- BR
end
L1 -. "NO es el mismo" .- L2
L1 -. "NO es el mismo" .- HL
Tres localhost distintos en la misma máquina. Cuando server.js intentaba conectarse a localhost:5432 en el módulo 2, estaba preguntando por PostgreSQL dentro de su propio contenedor, donde solo vive node. De ahí el ECONNREFUSED: alguien contestó (el propio contenedor) diciendo que ahí no hay nadie escuchando.
Compruébalo tú mismo:
docker exec aurora-api hostname -i
docker exec aurora-db hostname -i
docker exec aurora-api sh -c 'nc -z localhost 5432; echo "PostgreSQL en mi localhost: $?"'Cada uno con su IP, y en el localhost de la API no hay ninguna base de datos. La regla, en una línea:
Dentro de un contenedor,
localhostsiempre significa "yo mismo". Para hablar con otro contenedor hay que usar su dirección: su IP o, mucho mejor, su nombre.
- Los drivers de red integrados
Docker implementa las redes mediante drivers intercambiables:
| Driver | Qué hace | Aislamiento | Cuándo usarlo |
|---|---|---|---|
bridge |
Red virtual privada en el host; cada contenedor recibe una IP interna | Alto | Por defecto y para casi todo: aplicaciones de varios contenedores en una máquina |
host |
El contenedor comparte la pila de red del host: sin IP propia, sin NAT | Ninguno | Rendimiento extremo, o servicios que necesitan ver todos los puertos del host |
none |
Solo interfaz de loopback: sin red | Total | Trabajos por lotes que no necesitan red; máxima seguridad |
overlay |
Red que abarca varios hosts de un clúster | Alto | Docker Swarm. Se ve en la lección 06-03 |
macvlan |
El contenedor recibe una MAC y una IP de tu red física | Medio | Integrar contenedores en una LAN existente. Detalle en la lección 05-01 |
ipvlan |
Similar, compartiendo la MAC del host | Medio | Entornos con restricciones de MAC. Detalle en la lección 05-01 |
Las tres primeras vienen preconfiguradas y las ves con:
NETWORK ID NAME DRIVER SCOPE
6b2f8a1c9e73 bridge bridge local
f19d4c7e2a85 host host local
3a8e1b5f9c24 none null localUn vistazo rápido a host y none, que se usan poco pero conviene reconocer:
docker run --rm --network host alpine:3.20 hostname -i
docker run --rm --network none alpine:3.20 sh -c 'ip -o addr show | awk "{print \$2, \$4}"'Con --network host, el contenedor tiene la misma IP que tu máquina: no hay -p que valga porque los puertos son directamente los del host, y no hay ningún aislamiento de red. Con --network none, solo existe lo: ni siquiera puede hacer ping a nada.
bridge |
host |
none |
|
|---|---|---|---|
| IP propia | Sí | No, la del host | No |
Hace falta -p |
Sí | No, y no funciona | Irrelevante |
| Aislamiento de red | Sí | No | Total |
| Rendimiento | Muy bueno (hay NAT) | El máximo | — |
| Conflictos de puertos | Solo en el host | Directos entre contenedores | No |
| Disponible en Docker Desktop (macOS/Windows) | Sí | Con limitaciones | Sí |
- La bridge por defecto frente a las redes definidas por el usuario
Aquí está el corazón de la lección. Existen dos tipos de red bridge y se comportan de forma muy distinta:
Bridge por defecto (bridge, docker0) |
Bridge definida por el usuario | |
|---|---|---|
| Cómo se usa | Automática: todo contenedor sin --network cae aquí |
docker network create mi-red + --network mi-red |
| DNS interno entre contenedores | NO | SÍ: cada contenedor se resuelve por su nombre |
| Aislamiento | Todos los contenedores juntos, incluidos los de otros proyectos | Solo los que conectes a esa red |
| Conectar/desconectar en caliente | No | Sí, con network connect/disconnect |
| Alias de red | No | Sí, con --network-alias |
| Variables de entorno de enlace | Sistema --link, obsoleto |
No hace falta |
| Recomendación | Evitarla | Usarla siempre |
Solo la primera fila importa hoy, pero es determinante: en una red propia, Docker levanta un servidor DNS interno en 127.0.0.11 que resuelve los nombres de los contenedores conectados a esa red. En la bridge por defecto, ese DNS existe pero solo reenvía consultas a los servidores del host: no conoce ningún nombre de contenedor. Ese es, literalmente, todo el misterio que llevas arrastrando desde la lección 02-06.
Antes de arreglarlo, deja constancia del punto de partida:
- Los comandos de
docker network
docker network| Comando | Para qué |
|---|---|
docker network ls |
Listar redes |
docker network create |
Crear una red |
docker network inspect |
Ver su configuración y sus contenedores |
docker network connect |
Conectar un contenedor en marcha a una red |
docker network disconnect |
Desconectarlo |
docker network rm |
Borrar una red |
docker network prune |
Borrar todas las redes sin contenedores |
Crear una red
Con opciones, cuando necesites control:
docker network create \
--driver bridge \
--subnet 172.28.0.0/16 \
--gateway 172.28.0.1 \
--label proyecto=aurora-libros \
aurora-net-personalizada| Opción | Para qué |
|---|---|
--driver |
El driver; bridge es el valor por defecto |
--subnet |
Fijar el rango de IPs (útil si choca con tu VPN) |
--gateway |
La puerta de enlace de esa subred |
--ip-range |
Restringir el rango de asignación automática |
--internal |
Red sin salida a Internet: aislamiento total hacia fuera |
--label |
Etiquetas, como en los contenedores |
--attachable |
Permitir conectar contenedores sueltos a una red overlay |
--internal merece atención: una red interna permite que los contenedores se hablen entre sí pero les corta la salida al exterior. Es una defensa excelente para una base de datos que no tiene por qué salir a Internet, y se retoma en la lección 05-03.
Inspeccionar
docker network inspect aurora-net --format '{{.Name}} | driver={{.Driver}} | subred={{range .IPAM.Config}}{{.Subnet}}{{end}}'Y la consulta más útil de todas: quién está conectado.
docker network inspect aurora-net --format '{{range .Containers}}{{.Name}} → {{.IPv4Address}}{{println}}{{end}}'De momento no imprime nada: la red está vacía.
Borrar y limpiar
Una red con contenedores conectados no se puede borrar (network ... has active endpoints). Y las tres redes predefinidas —bridge, host, none— no se pueden eliminar nunca.
- Publicación de puertos, revisitada
Ahora que entiendes el modelo, la tabla de la lección 03-01 cobra sentido pleno:
| Sintaxis | Efecto |
|---|---|
-p 3000:3000 |
Puerto 3000 de todas las interfaces del host → 3000 del contenedor |
-p 127.0.0.1:5432:5432 |
Solo accesible desde el propio host |
-p 8080-8090:8080-8090 |
Un rango de puertos |
-p 5514:514/udp |
Publicación UDP |
-p 3000 |
Puerto aleatorio del host → 3000 del contenedor |
-P |
Publica todos los EXPOSE en puertos aleatorios |
| (nada) | El contenedor no es accesible desde el host, pero sí desde su red |
Y esta es la distinción que hay que tener absolutamente clara:
Publicar un puerto (-p) |
Comunicación entre contenedores | |
|---|---|---|
| Para qué sirve | Que el host y el mundo exterior alcancen el contenedor | Que un contenedor alcance a otro |
| Cómo se dirige | localhost:PUERTO_HOST desde el host |
nombre-contenedor:PUERTO_INTERNO |
¿Requiere -p? |
Sí, es su definición | No, en absoluto |
| Puerto que se usa | El del host (el de la izquierda) | El interno del contenedor, siempre |
| Requisito | Ninguno salvo que el puerto esté libre | Estar en la misma red definida por el usuario |
De ahí salen dos errores clásicos que ahora puedes evitar:
- Publicar puertos "para que los contenedores se vean". No hace falta y encima expone servicios internos.
aurora-dbno necesita ningún-ppara que la API la use; solo lo necesitas tú, si quieres conectarte con un cliente desde el host. - Usar el puerto del host en la cadena de conexión de otro contenedor. Si publicas
-p 5433:5432, otro contenedor debe seguir conectándose aaurora-db:5432, el puerto interno. El 5433 solo existe para el host.
- Práctica:
aurora-net y la resolución de nombres
aurora-net y la resolución de nombresManos a la obra. Primero, la red:
docker network create --label proyecto=aurora-libros aurora-net
docker network ls --filter name=aurora-netc7f2e9a13b8d46052fa8c1e7b3d9a4f6c2e0b8d5a3f1c9e7b5d3a1f9c7e5b3d1
NETWORK ID NAME DRIVER SCOPE
c7f2e9a13b8d aurora-net bridge localAhora recrea los tres servicios dentro de ella. Como la red es un ajuste de namespace, se fija al crear el contenedor (lección 03-01), así que hay que borrar y volver a crear:
docker run -d \
--name aurora-db \
--network aurora-net \
--label proyecto=aurora-libros --label componente=base-de-datos \
-e POSTGRES_USER=aurora \
-e POSTGRES_PASSWORD=aurora_secreta \
-e POSTGRES_DB=aurora_libros \
-p 127.0.0.1:5432:5432 \
postgres:16-alpinedocker run -d \
--name aurora-cache \
--network aurora-net \
--label proyecto=aurora-libros --label componente=cache \
redis:7-alpineFíjate en un cambio importante en aurora-cache: ya no lleva -p. Nadie desde el host necesita hablar con Redis; solo la API, y para eso basta con estar en la misma red. Es el principio de exponer lo mínimo imprescindible.
Antes de arrancar la API, vuelve a cargar el catálogo, porque el contenedor de la base de datos es nuevo:
sleep 5
docker cp ~/aurora-libros/db/init.sql aurora-db:/tmp/init.sql
docker exec aurora-db psql -U aurora -d aurora_libros -f /tmp/init.sql
docker exec aurora-db psql -U aurora -d aurora_libros -t -c "SELECT COUNT(*) FROM libros;"Anota esta molestia, porque es la segunda vez que la sufres: cada vez que recreas aurora-db pierdes los datos y tienes que volver a cargarlos a mano. Esa es la carencia que resuelve la lección 03-06.
Y ahora la API:
docker run -d \
--name aurora-api \
--network aurora-net \
--label proyecto=aurora-libros --label componente=api \
--env-file ~/aurora-libros/aurora.env \
-p 3000:3000 \
auroralibros/aurora-api:1.2.0Verifica la resolución de nombres
docker exec aurora-api getent hosts aurora-db
docker exec aurora-api getent hosts aurora-cache
docker exec aurora-api getent hosts aurora-apiAhí está. El mismo comando que hace diez minutos devolvía código 2 y ninguna línea, ahora resuelve los tres nombres. No has cambiado ni una línea de server.js, ni el Dockerfile, ni el fichero de entorno: solo has puesto los contenedores en una red propia.
Comprueba también la conectividad real, puerto a puerto:
docker run --rm --network aurora-net nicolaka/netshoot \
sh -c 'nc -zv aurora-db 5432; nc -zv aurora-cache 6379; nc -zv aurora-api 3000'Connection to aurora-db (172.18.0.2) 5432 port [tcp/postgresql] succeeded!
Connection to aurora-cache (172.18.0.3) 6379 port [tcp/redis] succeeded!
Connection to aurora-api (172.18.0.4) 3000 port [tcp/*] succeeded!Los tres, por nombre. Y observa el detalle: aurora-cache responde en el 6379 aunque no publicaste ningún puerto. La publicación es para el host; dentro de la red, todos los puertos del contenedor están disponibles para sus vecinos.
Y el estado de la flota:
docker ps --filter label=proyecto=aurora-libros --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"NAMES STATUS PORTS
aurora-api Up 30 seconds (healthy) 0.0.0.0:3000->3000/tcp
aurora-cache Up 1 minute 6379/tcp
aurora-db Up 2 minutes 127.0.0.1:5432->5432/tcp(healthy). Ese paréntesis lleva cuatro lecciones diciendo unhealthy. El HEALTHCHECK que escribiste en la lección 02-04 hace su comprobación contra /salud, /salud consulta PostgreSQL y Redis, ambos responden, y por primera vez devuelve 200. Y fíjate en la línea de aurora-cache: 6379/tcp a secas, sin flecha. Expuesto pero no publicado.
- El momento de la verdad:
/libros devuelve los ocho libros
/libros devuelve los ocho librosdb: ok. cache: ok. Y ahora, el comando que llevas todo el curso esperando:
{
"origen": "db",
"libros": [
{ "id": 3, "titulo": "Cien años de soledad", "autor": "Gabriel García Márquez", "isbn": "978-84-397-2071-7", "precio": "17.95" },
{ "id": 1, "titulo": "El jardín de senderos que se bifurcan", "autor": "Jorge Luis Borges", "isbn": "978-84-206-3312-1", "precio": "14.50" },
{ "id": 8, "titulo": "El tiempo entre costuras", "autor": "María Dueñas", "isbn": "978-84-8365-351-1", "precio": "20.15" },
{ "id": 6, "titulo": "La casa de los espíritus", "autor": "Isabel Allende", "isbn": "978-84-9838-618-3", "precio": "18.40" },
{ "id": 4, "titulo": "La sombra del viento", "autor": "Carlos Ruiz Zafón", "isbn": "978-84-08-04364-5", "precio": "21.00" },
{ "id": 7, "titulo": "Los detectives salvajes", "autor": "Roberto Bolaño", "isbn": "978-84-339-6835-7", "precio": "23.60" },
{ "id": 5, "titulo": "Nada", "autor": "Carmen Laforet", "isbn": "978-84-233-4361-2", "precio": "12.75" },
{ "id": 2, "titulo": "Rayuela", "autor": "Julio Cortázar", "isbn": "978-84-376-0494-7", "precio": "19.90" }
]
}Los ocho libros. El jardín de senderos que se bifurcan, Rayuela, Cien años de soledad, La sombra del viento, Nada, La casa de los espíritus, Los detectives salvajes y El tiempo entre costuras, ordenados por título, servidos por una API en un contenedor, leídos de una base de datos PostgreSQL en otro contenedor, a través de una red virtual que has creado tú. Párate un segundo en esto: la plataforma de Aurora Libros está viva. Es la primera vez desde la lección 01-07, donde la ejecutabas a mano en quince pasos y fallaba, que el sistema completo funciona de extremo a extremo.
Y hay una segunda comprobación, más sutil, que demuestra que la caché también está haciendo su trabajo:
curl -s http://localhost:3000/libros | jq -r '.origen'
curl -s http://localhost:3000/libros | jq -r '.origen'
docker exec aurora-cache redis-cli KEYS '*'
docker exec aurora-cache redis-cli TTL libros:todosLa primera llamada fue a PostgreSQL y guardó el resultado en Redis con un TTL de 60 segundos; la segunda ya lo sirvió desde la caché. Los tres contenedores están cooperando: la API habla con la base de datos por su nombre y con la caché por el suyo. Compruébalo también por el otro extremo:
{
"id": 2,
"titulo": "Rayuela",
"autor": "Julio Cortázar",
"isbn": "978-84-376-0494-7",
"precio": "19.90"
}Qué ha cambiado exactamente
| Antes (bridge por defecto) | Ahora (aurora-net) |
|
|---|---|---|
getent hosts aurora-db |
Nada, código 2 | 172.18.0.2 aurora-db |
/salud |
503, db: ko, cache: ko |
200, db: ok, cache: ok |
| Estado del contenedor | Up (unhealthy) |
Up (healthy) |
/libros |
{"error": "No se pudo obtener el catálogo"} |
Los ocho libros |
| Cambios en el código | — | Ninguno |
Esa última fila es la lección que hay que llevarse: el problema nunca estuvo en la aplicación ni en la imagen. Estaba en cómo se conectaban los contenedores entre sí.
aurora-web: Nginx como proxy inverso
aurora-web: Nginx como proxy inversoFalta el cuarto servicio. Tu web/index.html de la lección 01-07 llama a /api/libros, una ruta relativa, no a http://localhost:3000/libros. Ese diseño era deliberado: el navegador debe hablar con un único origen, y quien reparte por dentro es Nginx.
Crea ~/aurora-libros/web/nginx.conf:
server {
listen 80;
server_name _;
# Docker resuelve los nombres de contenedor en 127.0.0.11.
# Con "valid=10s" Nginx reconsulta el DNS y no se queda con una IP vieja
# si aurora-api se recrea y cambia de dirección.
resolver 127.0.0.11 valid=10s;
# 1) La web estática
location / {
root /usr/share/nginx/html;
index index.html;
}
# 2) Todo lo que empiece por /api/ se reenvía a aurora-api
location /api/ {
set $api_upstream http://aurora-api:3000;
proxy_pass $api_upstream/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
}Línea a línea, lo que importa:
| Directiva | Qué hace |
|---|---|
listen 80 |
Nginx escucha en el 80 dentro del contenedor |
resolver 127.0.0.11 valid=10s |
Usa el DNS interno de Docker y refresca la resolución cada 10 s |
set $api_upstream ... + proxy_pass $api_upstream/ |
Al usar una variable, Nginx resuelve el nombre en cada petición en lugar de solo al arrancar. Sin este truco, si recreas aurora-api y cambia su IP, el proxy sigue apuntando a la vieja y devuelve 502 |
La / final de proxy_pass ...$api_upstream/ |
Recorta el prefijo: /api/libros se reenvía como /libros, que es la ruta que expone la API |
X-Forwarded-For y compañía |
Cabeceras estándar para que la API sepa quién es el cliente original |
proxy_connect_timeout 5s |
No dejar peticiones colgadas eternamente si la API no responde |
Arranca el contenedor:
docker run -d \
--name aurora-web \
--network aurora-net \
--label proyecto=aurora-libros --label componente=web \
-p 8080:80 \
-v ~/aurora-libros/web/index.html:/usr/share/nginx/html/index.html:ro \
-v ~/aurora-libros/web/nginx.conf:/etc/nginx/conf.d/default.conf:ro \
--stop-signal SIGQUIT \
nginx:alpineDos detalles del comando:
- Los dos
-vmontan ficheros del host en solo lectura (:ro). Es la sintaxis mínima de la lección 03-01; la lección 03-06 explica qué son exactamente estos montajes y por qué:roes importante en ficheros de configuración. --stop-signal SIGQUITaplica lo que aprendiste en la lección 02-04: Nginx hace un apagado ordenado con SIGQUIT y uno abrupto con SIGTERM. Con esta opción,docker stop aurora-webno corta peticiones a medio servir.
Comprueba la cadena completa:
curl -s -o /dev/null -w "web estática: HTTP %{http_code}\n" http://localhost:8080/
curl -s http://localhost:8080/api/libros | jq -r '.libros[] | .titulo' | head -4
curl -s http://localhost:8080/api/salud | jq -cweb estática: HTTP 200
Cien años de soledad
El jardín de senderos que se bifurcan
El tiempo entre costuras
La casa de los espíritus
{"servicio":"aurora-api","version":"1.0.0","db":"ok","cache":"ok"}Abre http://localhost:8080 en el navegador. La página de Aurora Libros carga, llama a /api/libros, Nginx la reenvía a aurora-api, la API consulta PostgreSQL o Redis, y la tabla se rellena con los ocho títulos y sus precios en euros. La arquitectura completa, funcionando:
flowchart LR
N["Navegador<br/>localhost:8080"] --> W
subgraph NET["Red aurora-net (172.18.0.0/16)"]
W["aurora-web<br/>nginx:alpine<br/>:80"]
A["aurora-api<br/>node 22<br/>:3000"]
D["aurora-db<br/>postgres 16<br/>:5432"]
C["aurora-cache<br/>redis 7<br/>:6379"]
W -- "/api/ → http://aurora-api:3000/" --> A
A -- "aurora-db:5432" --> D
A -- "aurora-cache:6379" --> C
end
Ni una sola IP escrita a mano en ninguna parte: aurora-web busca aurora-api, y aurora-api busca aurora-db y aurora-cache, todos por nombre.
- Conectar y desconectar redes en caliente
Con redes definidas por el usuario, y solo con ellas, puedes cambiar la topología sin recrear nada:
docker network create aurora-admin
docker network connect aurora-admin aurora-db
docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}={{$v.IPAddress}} {{end}}' aurora-dbaurora-db tiene ahora dos interfaces y dos IPs, una en cada red. Es el patrón habitual para dar acceso a una herramienta de administración sin abrir la red de la aplicación:
docker run -d --name aurora-adminer --network aurora-admin -p 8081:8080 adminer:latest
docker exec aurora-adminer getent hosts aurora-dbAdminer ve la base de datos, pero no ve aurora-api ni aurora-cache, porque no está en aurora-net. Aislamiento por diseño.
docker network disconnect aurora-admin aurora-db
docker rm -f aurora-adminer
docker network rm aurora-adminY un apunte sobre alias de red, que serán importantes en el módulo 4:
docker run -d --name aurora-db-nuevo --network aurora-net \
--network-alias base-de-datos --network-alias postgres \
-e POSTGRES_PASSWORD=aurora_secreta postgres:16-alpine
docker exec aurora-api getent hosts base-de-datos
docker rm -f aurora-db-nuevoUn contenedor puede responder a varios nombres en la misma red. Es lo que permite, por ejemplo, cambiar de implementación sin tocar la configuración de quien la consume.
- Buenas prácticas de red
| Práctica | Por qué |
|---|---|
| Una red por aplicación | aurora-net contiene solo los cuatro servicios de Aurora Libros. Otro proyecto, otra red. Sin contactos accidentales |
| Nunca uses la bridge por defecto | Sin DNS, sin aislamiento y con todos los contenedores de la máquina juntos |
| Publica solo lo que necesite el exterior | aurora-web en el 8080 y ya está. aurora-cache no lleva -p, y aurora-db solo en 127.0.0.1 |
Ata las bases de datos a 127.0.0.1 |
-p 127.0.0.1:5432:5432 te deja usar un cliente local sin exponer PostgreSQL a la wifi |
| Refiérete a los servicios por nombre, jamás por IP | Las IPs cambian en cada arranque; los nombres no |
| Usa el puerto interno en las cadenas de conexión | Si publicas -p 5433:5432, otro contenedor sigue usando aurora-db:5432 |
Considera --internal para las redes de datos |
Corta la salida a Internet de servicios que no la necesitan |
| Etiqueta las redes | --label proyecto=aurora-libros permite limpiar sin destrozar lo ajeno |
Cómo queda la superficie expuesta de Aurora Libros:
NAMES PORTS
aurora-web 0.0.0.0:8080->80/tcp
aurora-api 0.0.0.0:3000->3000/tcp
aurora-cache 6379/tcp
aurora-db 127.0.0.1:5432->5432/tcpEn un despliegue real, incluso el -p 3000:3000 de la API sobraría: si el navegador solo habla con Nginx, la API no necesita estar publicada. Puedes probarlo recreándola sin -p y verás que http://localhost:8080/api/libros sigue funcionando perfectamente, porque el proxy la alcanza por la red interna.
Lo que esta lección no cubre, y dónde se ve: cómo funciona el bridge por dentro (parejas veth, iptables, nat), las redes overlay entre varios hosts, macvlan y las políticas de red se estudian en la lección 05-01 y en la 06-03.
Errores Comunes y Consejos
- Usar
localhostpara referirse a otro contenedor. Dentro de un contenedor,localhostes él mismo. Usa el nombre del otro contenedor. - Dejar los contenedores en la bridge por defecto y esperar que se resuelvan por nombre. No ocurrirá jamás. Crea una red con
docker network create. - Escribir IPs a mano. Funciona hasta el siguiente reinicio. Docker asigna las IPs por orden de arranque.
- Usar el puerto publicado del host en la conexión entre contenedores. Con
-p 5433:5432, otro contenedor debe seguir usando el 5432. - Publicar puertos "para que se vean entre ellos". No hace falta, y expone servicios internos a toda la red.
- Intentar cambiar de red con
docker start. La red se fija al crear. Se cambia connetwork connect/disconnect, o recreando. - 502 en Nginx tras recrear la API. Nginx cacheó la IP antigua. Se resuelve con
resolver 127.0.0.11 valid=10syproxy_passsobre una variable, como en la configuración de esta lección. - Olvidar la
/final deproxy_pass. Sin ella,/api/librosse reenvía como/api/librosy la API responde 404. Con ella, llega como/libros. - Consejo:
docker network inspectes la forma más rápida de responder a "¿quién está en esta red y con qué IP?". - Consejo: para diagnosticar, lanza
netshootconectado a la red (--network aurora-net) y pruebanc,digycurldesde dentro de ella.
Ejercicios
Ejercicio 1: demuestra el aislamiento entre redes
Crea dos redes, red-a y red-b. Levanta un contenedor servidor-a con nginx:alpine en red-a, y dos clientes de nicolaka/netshoot: cliente-a en red-a y cliente-b en red-b. Demuestra con comandos que:
cliente-aresuelveservidor-apor nombre y obtiene un HTTP 200.cliente-bno lo resuelve ni lo alcanza, ni por nombre ni por IP.- Después, sin recrear nada, haz que
cliente-bpueda alcanzarlo, y verifica cuántas IPs tiene entonces.
Ejercicio 2: publicar frente a comunicar
Levanta una pila mínima en una red red-tienda: un mini-db con redis:7-alpine sin ningún -p, y un mini-api con nicolaka/netshoot que ejecute sleep 3600, publicando -p 9999:80. Responde con comandos y explicaciones:
- ¿Puede
mini-apihablar conmini-db? Demuéstralo. - ¿Puedes tú, desde el host, hablar con
mini-dbconredis-clionc? ¿Por qué? - ¿Qué responde
curl localhost:9999y por qué, simini-apino tiene ningún servidor web? - Recrea
mini-dbcon-p 127.0.0.1:6380:6379y repite el punto 2. ¿Ha cambiado algo paramini-api?
Ejercicio 3: rompe y arregla el proxy inverso
Con la plataforma de Aurora Libros funcionando, provoca y diagnostica dos averías reales del proxy:
- Cambia en
nginx.confel destino ahttp://aurora-api-inexistente:3000, recarga Nginx condocker kill -s SIGHUP aurora-weby observa qué devuelvecurl localhost:8080/api/libros. Diagnostica condocker logs aurora-web. - Restaura el destino correcto pero quita la barra final de
proxy_pass. Recarga y observa el nuevo error. Explica exactamente qué ruta le está llegando aaurora-api(compruébalo condocker logs aurora-api). - Deja todo correcto y demuestra, recreando
aurora-apisin-p, que la web sigue funcionando enlocalhost:8080.
Soluciones
Solución al ejercicio 1
docker network create red-a
docker network create red-b
docker run -d --name servidor-a --network red-a nginx:alpine
docker run -d --name cliente-a --network red-a nicolaka/netshoot sleep 3600
docker run -d --name cliente-b --network red-b nicolaka/netshoot sleep 36001. Desde cliente-a:
docker exec cliente-a getent hosts servidor-a
docker exec cliente-a curl -s -o /dev/null -w "HTTP %{http_code}\n" http://servidor-a2. Desde cliente-b:
docker exec cliente-b nslookup servidor-a 2>&1 | tail -2
docker exec cliente-b sh -c 'nc -zv -w 3 172.20.0.2 80; echo "código: $?"'** server can't find servidor-a: NXDOMAIN
nc: connect to 172.20.0.2 port 80 (tcp) failed: Connection timed out
código: 1Aquí hay dos fallos distintos y ambos importan: el nombre no se resuelve (no comparten red, así que el DNS de red-b no conoce a servidor-a) y tampoco funciona la IP, porque son dos bridges separadas y el tráfico entre ellas no está permitido. Compáralo con el diagnóstico de la lección 03-04: allí el nombre fallaba pero la IP funcionaba, lo que demostraba que sí compartían red. La combinación de las dos pruebas es lo que distingue "misma red sin DNS" de "redes distintas".
3. Conectar en caliente:
docker network connect red-a cliente-b
docker exec cliente-b curl -s -o /dev/null -w "HTTP %{http_code}\n" http://servidor-a
docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}={{$v.IPAddress}} {{end}}' cliente-bDos IPs, una por red, sin haber parado ni recreado el contenedor. Eso solo es posible con redes definidas por el usuario.
Solución al ejercicio 2
docker network create red-tienda
docker run -d --name mini-db --network red-tienda redis:7-alpine
docker run -d --name mini-api --network red-tienda -p 9999:80 nicolaka/netshoot sleep 36001.
Sí, perfectamente, y mini-db no publica ningún puerto. Dentro de una red compartida, todos los puertos del contenedor están disponibles para sus vecinos: -p no tiene nada que ver con esto.
2.
No. Como mini-db no publicó ningún puerto, no existe ninguna regla que reenvíe tráfico del host hacia él. El host y la red de Docker son dos ámbitos distintos: el host solo alcanza lo que se ha publicado explícitamente.
3.
La publicación existe —docker ps muestra 0.0.0.0:9999->80/tcp—, pero dentro del contenedor no hay nada escuchando en el 80: mini-api solo ejecuta sleep. Es la demostración de que -p no comprueba nada: crea el reenvío aunque el destino esté vacío. Es el mismo error que se comete al invertir el orden en -p 80:8080.
4.
docker rm -f mini-db
docker run -d --name mini-db --network red-tienda -p 127.0.0.1:6380:6379 redis:7-alpine
redis-cli -p 6380 PING 2>/dev/null || docker run --rm --network host redis:7-alpine redis-cli -p 6380 PING
docker exec mini-api redis-cli -h mini-db PINGAhora tú puedes conectarte desde el host por el puerto 6380, y para mini-api no ha cambiado absolutamente nada: sigue usando mini-db:6379, el puerto interno. Los dos caminos son independientes.
Solución al ejercicio 3
1. Destino inexistente:
sed -i 's|http://aurora-api:3000|http://aurora-api-inexistente:3000|' ~/aurora-libros/web/nginx.conf
docker kill -s SIGHUP aurora-web
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8080/api/libros
docker logs --tail 3 aurora-webHTTP 502
2026/08/04 21:47:12 [error] 31#31: *5 aurora-api-inexistente could not be resolved (3: Host not found),
client: 172.18.0.1, server: _, request: "GET /api/libros HTTP/1.1", host: "localhost:8080"502 Bad Gateway es el error de "el proxy no pudo hablar con el destino", y el log da el motivo exacto: could not be resolved. Es el mismo NXDOMAIN de la lección 03-04, visto desde Nginx. Y merece la pena notar que la web estática sigue sirviéndose sin problema en http://localhost:8080/: solo falla la ruta /api/.
2. Sin la barra final:
sed -i 's|http://aurora-api-inexistente:3000|http://aurora-api:3000|' ~/aurora-libros/web/nginx.conf
sed -i 's|proxy_pass $api_upstream/;|proxy_pass $api_upstream;|' ~/aurora-libros/web/nginx.conf
docker kill -s SIGHUP aurora-web
curl -s -w "\nHTTP %{http_code}\n" http://localhost:8080/api/libros
docker logs --tail 2 aurora-apiAhora sí llega a la API, pero con la ruta equivocada. Con la barra final, proxy_pass http://aurora-api:3000/ sustituye el prefijo /api/ por /, así que /api/libros llega como /libros. Sin ella, Nginx concatena la ruta completa y la API recibe /api/libros, una ruta que server.js no define, de ahí el 404 de Express. Un solo carácter, y la mitad de los proxies mal configurados del mundo se explican por él.
3. Restaurar y quitar la publicación de la API:
sed -i 's|proxy_pass $api_upstream;|proxy_pass $api_upstream/;|' ~/aurora-libros/web/nginx.conf
docker kill -s SIGHUP aurora-web
docker rm -f aurora-api
docker run -d --name aurora-api --network aurora-net \
--label proyecto=aurora-libros --label componente=api \
--env-file ~/aurora-libros/aurora.env \
auroralibros/aurora-api:1.2.0
sleep 5
curl -s -o /dev/null -w "API directa (3000): HTTP %{http_code}\n" http://localhost:3000/libros
curl -s http://localhost:8080/api/libros | jq -r '.libros | length'Justo lo que se buscaba: desde el host, el puerto 3000 ya no existe; desde la web, los ocho libros siguen llegando. La API ha dejado de estar expuesta y el servicio no se ha resentido, porque Nginx la alcanza por la red interna. Es exactamente la arquitectura que querrías en producción: una sola puerta de entrada.
Como en las siguientes lecciones conviene poder llamar a la API directamente para hacer pruebas, vuelve a dejarla publicada:
docker rm -f aurora-api
docker run -d --name aurora-api --network aurora-net \
--label proyecto=aurora-libros --label componente=api \
--env-file ~/aurora-libros/aurora.env -p 3000:3000 \
auroralibros/aurora-api:1.2.0Conclusión
El misterio está resuelto y la plataforma está viva. Sabes que cada contenedor tiene su propia pila de red y su propio localhost, y que por eso localhost:5432 dentro de la API preguntaba por una base de datos que nunca estuvo ahí. Conoces los drivers integrados —bridge para casi todo, host cuando no quieres aislamiento ni NAT, none para aislamiento total— y sabes que overlay, macvlan e ipvlan existen para escenarios de varios hosts y de integración con la red física que verás en las lecciones 05-01 y 06-03.
Y tienes clavada la diferencia que lo explicaba todo: la bridge por defecto no tiene DNS interno; una red definida por el usuario sí. Un docker network create aurora-net y un --network aurora-net en cada contenedor, y getent hosts aurora-db pasó de devolver código 2 a devolver 172.18.0.2 aurora-db. Sin tocar una línea de server.js, ni el Dockerfile, ni el fichero de entorno. También sabes conectar y desconectar redes en caliente para dar acceso puntual a una herramienta de administración, y poner alias de red a un contenedor.
Distingues publicar un puerto de comunicar dos contenedores: -p es para que el host y el exterior entren; dentro de la red, todos los puertos están disponibles por nombre sin publicar nada. Por eso aurora-cache ya no lleva -p, aurora-db está atada a 127.0.0.1, y has demostrado que hasta la API puede vivir sin publicación si Nginx es la única puerta de entrada.
El resultado, en una línea: curl http://localhost:3000/libros devuelve los ocho libros de Aurora Libros, servidos por Node desde PostgreSQL, cacheados en Redis con su TTL de 60 segundos, y http://localhost:8080 muestra la librería completa en el navegador a través de un proxy inverso que reenvía /api/ sin exponer la API. El healthcheck marca healthy por primera vez en todo el curso. Cuatro contenedores, una red, cero IPs escritas a mano.
Queda un problema, y ya lo has sufrido dos veces en esta misma lección: cada vez que recreas aurora-db, los ocho libros desaparecen y tienes que volver a cargar init.sql con docker cp. Los datos viven en un volumen anónimo que nadie controla, o directamente en la capa de escritura que muere con el contenedor. En la siguiente lección, Persistencia de Datos con Volúmenes, atacarás ese problema de raíz: verás los tres tipos de montaje —volúmenes gestionados, bind mounts y tmpfs—, la diferencia entre -v y --mount, dónde viven realmente los volúmenes y por qué no debes tocarlos a mano. Le darás a PostgreSQL el volumen aurora-datos, montarás init.sql para que se ejecute solo, y repetirás la prueba definitiva: insertar un libro nuevo, destruir el contenedor de la base de datos, recrearlo, y comprobar que el libro sigue ahí.
Docker: De Principiante a Avanzado
Módulo 1: Introducción a Docker
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
- El Proyecto del Curso: la Plataforma Aurora Libros
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Instrucciones Avanzadas del Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Inspección y Depuración de Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
- Límites de Recursos y Políticas de Reinicio
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
- Perfiles, Overrides y Múltiples Entornos
- Desarrollo Local con Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Builds Avanzadas con BuildKit y Buildx
- Registro y Monitoreo en Docker
- El Runtime por Dentro: Namespaces, Cgroups y Capas
Módulo 6: Docker en Producción
- Preparar una Imagen para Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
- Estrategias de Despliegue y Rollback
