Tienes los cuatro servicios declarados y dominas la CLI. Falta lo que de verdad convierte cuatro contenedores en una aplicación: cómo se encuentran entre sí, qué puede hablar con qué, y en qué orden deben estar listos.
En esta lección montas la pila definitiva de Aurora Libros: DNS interno, dos redes segmentadas para que la web no pueda tocar la base de datos, arranque coordinado con sondas de salud reales, un servicio de migración que corre una vez y termina, y el patrón de reintento que hace falta aunque todo lo anterior esté bien. Al final, los quince pasos de onboarding de la lección 01-07 se habrán quedado en dos.
Contenido
- La arquitectura final
- Cómo se encuentran los servicios: el DNS de Compose
- Quién habla con quién y por qué puerto
- Segmentación en dos redes
- Demostración: la web no llega a la base de datos
- El orden de arranque: por qué
depends_ona secas no basta - Sondas de salud reales y
condition: service_healthy - Un servicio que corre una vez:
service_completed_successfully - Por qué la aplicación debe reintentar igualmente
- El
compose.yamlcompleto y comentado - Puesta en marcha end-to-end
- El recuento del onboarding
- La arquitectura final
graph TB
U["Navegador<br/>localhost:8080"] --> W
subgraph FRONTAL["red frontal (bridge)"]
W["aurora-web<br/>nginx:alpine<br/>proxy inverso"]
A1["aurora-api"]
end
subgraph TRASERA["red trasera (internal: true)"]
A2["aurora-api<br/>node:22-alpine :3000"]
D["aurora-db<br/>postgres:16 :5432"]
C["aurora-cache<br/>redis:7 :6379"]
M["aurora-migraciones<br/>corre una vez"]
end
W -->|"proxy /api → aurora-api:3000"| A1
A1 -.mismo contenedor.- A2
A2 -->|"SQL :5432"| D
A2 -->|"cache-aside :6379"| C
M -->|"aplica el esquema"| D
D --> V[("volumen<br/>aurora-datos")]
aurora-api es el único servicio que está en las dos redes: es la frontera. Todo lo que entra desde fuera pasa por aurora-web, y todo lo que toca los datos vive en una red sin salida al exterior.
- Cómo se encuentran los servicios: el DNS de Compose
En la lección 03-05 viste que una red bridge de usuario incorpora un servidor DNS interno en 127.0.0.11. Compose se apoya exactamente en eso, con una precisión que conviene grabar:
El nombre de host de un servicio es el nombre del servicio, no el del contenedor.
El contenedor se llama aurora-libros-aurora-db-1, pero desde la API te conectas a aurora-db. Compose registra en el DNS de cada red:
| Nombre registrado | Origen |
|---|---|
aurora-db |
Nombre del servicio (siempre) |
base-de-datos |
Cualquier aliases que declares |
aurora-libros-aurora-db-1 |
Nombre del contenedor |
Los alias resultan muy útiles al migrar: si el código heredado busca postgres, añades aliases: [postgres] y funciona sin tocar una línea de la aplicación.
Una consecuencia importante: la resolución es por red. Un servicio solo resuelve los nombres de los servicios con los que comparte alguna red. Es la base de la segmentación del apartado 4.
- Quién habla con quién y por qué puerto
Aquí está el error conceptual más repetido con Compose: usar el puerto publicado en lugar del interno. La publicación (ports) es una puerta desde el host hacia dentro; entre contenedores no interviene para nada.
| Origen | Destino | Host que usa | Puerto | Red |
|---|---|---|---|---|
| Navegador | aurora-web |
localhost |
8080 (publicado) | — |
aurora-web |
aurora-api |
aurora-api |
3000 (interno) | frontal |
aurora-api |
aurora-db |
aurora-db |
5432 (interno) | trasera |
aurora-api |
aurora-cache |
aurora-cache |
6379 (interno) | trasera |
aurora-migraciones |
aurora-db |
aurora-db |
5432 (interno) | trasera |
Solo la primera fila usa un puerto publicado, porque es la única que entra desde el host. El resto viaja por la red interna de Docker.
Esto se refleja en el web/nginx.conf que ya escribiste en el módulo 3, donde proxy_pass http://aurora-api:3000/; usa nombre de servicio y puerto interno. Y en las variables de la API: DB_HOST=aurora-db, REDIS_HOST=aurora-cache. Nada de localhost, nada de direcciones IP, nada de puertos publicados.
- Segmentación en dos redes
Con una sola red, los cuatro servicios se ven entre sí. Funciona, pero significa que si alguien compromete el contenedor de nginx —el más expuesto, el único que recibe tráfico de fuera— tiene acceso directo al puerto 5432 de PostgreSQL. La buena práctica es segmentar por zonas:
| Red | internal |
Servicios | Función |
|---|---|---|---|
frontal |
no | aurora-web, aurora-api |
Tráfico de entrada y proxy inverso |
trasera |
sí | aurora-api, aurora-db, aurora-cache, aurora-migraciones |
Datos, sin salida a Internet |
internal: true hace dos cosas: los contenedores de esa red no tienen ruta hacia el exterior (ni siquiera pueden hacer ping a Internet) y nadie desde fuera del host puede alcanzarlos. Si una dependencia comprometida de PostgreSQL intentara llamar a casa, no tendría por dónde.
Efecto colateral que conviene conocer: un contenedor conectado solo a redes internas no puede publicar puertos en el host. Por eso aurora-db deja de publicar 127.0.0.1:5432; para acceder a la base de datos usarás docker compose exec, y el entorno de desarrollo (lección 04-07) volverá a exponerla añadiendo el servicio a una red no interna.
- Demostración: la web no llega a la base de datos
La segmentación no es teoría: se comprueba.
docker compose exec aurora-web getent hosts aurora-api
docker compose exec aurora-web getent hosts aurora-dbLa primera resuelve; la segunda no devuelve nada y termina con código 2. aurora-web y aurora-db no comparten ninguna red, así que para nginx la base de datos sencillamente no existe en el DNS. Con la caja de herramientas de red de la lección 03-04, la diferencia entre las dos redes es tajante:
docker run --rm --network aurora-libros_frontal nicolaka/netshoot nc -zv -w 2 aurora-db 5432
docker run --rm --network aurora-libros_trasera nicolaka/netshoot nc -zv -w 2 aurora-db 5432
docker compose exec aurora-db ping -c 1 -W 2 8.8.8.8nc: getaddrinfo for host "aurora-db" port 5432: Name does not resolve
Connection to aurora-db (172.21.0.3) 5432 port [tcp/*] succeeded!
ping: sendto: Network is unreachableDesde frontal el nombre ni siquiera resuelve; desde trasera conecta; y la propia base de datos no tiene salida a Internet. Esa es la diferencia entre "funciona" y "funciona y además contiene el daño". El endurecimiento completo llega en la lección 05-03.
- El orden de arranque: por qué
depends_on a secas no basta
depends_on a secas no bastaPrueba lo que pasa sin ninguna condición, con depends_on: [aurora-db] en la API:
aurora-api-1 | Error: connect ECONNREFUSED 172.21.0.3:5432
aurora-api-1 | Fallo al conectar con la base de datos, saliendo
aurora-api-1 | exited with code 1Compose ha cumplido su promesa: arrancó aurora-db antes. El problema es qué significa "antes". Hay tres estados distintos y solo el tercero sirve:
| Estado | Significa | ¿Acepta conexiones? |
|---|---|---|
| Creado | El contenedor existe | No |
| Arrancado | El proceso está corriendo (depends_on corto) |
Todavía no |
Listo (healthy) |
La sonda de salud pasa | Sí |
PostgreSQL tarda entre 3 y 10 segundos desde que el proceso arranca hasta que acepta conexiones, y en el primer arranque, con init.sql por ejecutar, bastante más. La distinción entre "arrancado" y "listo" es la que rompe la mitad de los compose.yaml que circulan por ahí.
- Sondas de salud reales y
condition: service_healthy
condition: service_healthyLa solución tiene dos mitades. Primera: sondas que digan la verdad sobre cada servicio.
| Servicio | test |
Por qué esa comprobación |
|---|---|---|
aurora-db |
pg_isready -U aurora -d aurora_libros |
Herramienta oficial de PostgreSQL: devuelve 0 solo cuando acepta conexiones en esa base. Durante la inicialización, el puerto está abierto pero las conexiones se rechazan |
aurora-cache |
redis-cli ping |
Devuelve PONG y código 0 solo si el servidor responde de verdad |
aurora-api |
wget --spider -q http://localhost:3000/salud |
El endpoint propio, que a su vez comprueba base de datos y caché |
aurora-web |
wget --spider -q http://localhost/ |
nginx sirve la página |
Segunda mitad: declarar la dependencia de la salud, no del arranque.
aurora-api:
depends_on:
aurora-db:
condition: service_healthy
aurora-cache:
condition: service_healthyAhora Compose no lanza la API hasta que ambas sondas pasan. Si una nunca llega a healthy, up --wait falla con un mensaje claro en lugar de dejarte un servicio reiniciándose en bucle.
- Un servicio que corre una vez:
service_completed_successfully
service_completed_successfullyAurora Libros necesita aplicar el esquema y las migraciones antes de que la API atienda peticiones. Eso no es un servicio permanente: es una tarea que corre, termina y desaparece.
aurora-migraciones:
image: auroralibros/aurora-api:1.2.0 # misma imagen, otro comando
command: ["node", "migrar.js"]
environment:
DB_HOST: aurora-db
DB_USER: aurora
DB_PASSWORD: aurora_secreta
DB_NAME: aurora_libros
depends_on:
aurora-db:
condition: service_healthy
restart: "no" # que NO se reinicie al terminar
networks: [trasera]Y la API espera a que termine bien:
Dos detalles que se pasan por alto: restart: "no" es imprescindible —con unless-stopped heredado de un ancla, el contenedor se reiniciaría en bucle eternamente al terminar—, y las comillas también, porque no sin ellas es el booleano false en YAML. Además, la condición exige código de salida 0: si la migración falla, la API ni siquiera se crea, y ese es justo el comportamiento correcto.
- Por qué la aplicación debe reintentar igualmente
Con todo lo anterior, el arranque en frío está resuelto. Pero no los demás casos: si aurora-db se reinicia a las tres de la mañana por un fallo de disco, la API ya está corriendo y depends_on no vuelve a evaluarse; en un orquestador, los contenedores se reprograman en cualquier orden; y una caída de red de dos segundos no la arregla ningún fichero YAML.
depends_on resuelve el arranque; la reconexión es responsabilidad de la aplicación. Añade a api/server.js un reintento con retroceso exponencial:
async function conectarConReintentos(crearConexion, { intentos = 8, baseMs = 500 } = {}) {
for (let i = 1; i <= intentos; i++) {
try {
return await crearConexion();
} catch (err) {
if (i === intentos) throw err;
// 0,5 s · 1 s · 2 s · 4 s ... con jitter para no sincronizar reintentos
const espera = Math.min(baseMs * 2 ** (i - 1), 30_000) + Math.random() * 250;
console.warn(`Conexión fallida (intento ${i}/${intentos}): ${err.message}. ` +
`Reintento en ${Math.round(espera)} ms`);
await new Promise((r) => setTimeout(r, espera));
}
}
}
const pool = await conectarConReintentos(async () => {
const p = new Pool({ host: process.env.DB_HOST, user: process.env.DB_USER,
password: process.env.DB_PASSWORD, database: process.env.DB_NAME });
await p.query('SELECT 1'); // no basta con crear el pool: hay que probarlo
return p;
});Tres decisiones de diseño: el retroceso exponencial evita machacar una base de datos que está arrancando, el jitter aleatorio impide que diez réplicas reintenten al unísono, y la prueba SELECT 1 es lo que distingue "tengo un objeto de conexión" de "la base de datos me responde". La caché merece un trato distinto: si Redis no está, /libros debe seguir sirviendo desde PostgreSQL con origen: db. Una caché es un acelerador, nunca una dependencia dura.
- El
compose.yaml completo y comentado
compose.yaml completo y comentado# compose.yaml — Aurora Libros S.L. — plataforma completa
name: aurora-libros
x-comun: &comun
restart: unless-stopped
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
labels: { com.auroralibros.proyecto: "aurora-libros" }
services:
# --- Datos: PostgreSQL con el catálogo ---------------------------------
aurora-db:
<<: *comun
image: postgres:16-alpine
environment:
POSTGRES_USER: aurora
POSTGRES_PASSWORD: aurora_secreta
POSTGRES_DB: aurora_libros
volumes:
- aurora-datos:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U aurora -d aurora_libros"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s # la primera inicialización ejecuta init.sql
stop_grace_period: 30s # margen para cerrar el checkpoint
deploy:
resources: { limits: { memory: 512M, cpus: "1.0", pids: 200 }, reservations: { memory: 256M } }
networks: [trasera] # SOLO en la red interna
# --- Caché: Redis en modo cache-aside ----------------------------------
aurora-cache:
<<: *comun
image: redis:7-alpine
command: ["redis-server", "--maxmemory", "200mb", "--maxmemory-policy", "allkeys-lru"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
deploy:
resources: { limits: { memory: 256M, cpus: "0.5", pids: 100 } }
networks: [trasera]
# --- Migraciones: corre una vez y termina ------------------------------
aurora-migraciones:
image: auroralibros/aurora-api:1.2.0
command: ["node", "migrar.js"]
environment:
DB_HOST: aurora-db
DB_USER: aurora
DB_PASSWORD: aurora_secreta
DB_NAME: aurora_libros
depends_on:
aurora-db: { condition: service_healthy }
restart: "no" # entrecomillado: sin comillas sería el booleano false
networks: [trasera]
# --- API: la frontera entre las dos redes ------------------------------
aurora-api:
<<: *comun
build:
context: ./api
dockerfile: Dockerfile
image: auroralibros/aurora-api:1.2.0
environment:
PORT: "3000"
DB_HOST: aurora-db # nombre de SERVICIO
DB_USER: aurora
DB_PASSWORD: aurora_secreta
DB_NAME: aurora_libros
REDIS_HOST: aurora-cache
depends_on:
aurora-db: { condition: service_healthy }
aurora-cache: { condition: service_healthy }
aurora-migraciones: { condition: service_completed_successfully }
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/salud"]
interval: 10s
timeout: 3s
retries: 3
start_period: 20s
restart: on-failure:3
deploy:
resources: { limits: { memory: 256M, cpus: "1.0", pids: 100 }, reservations: { memory: 128M } }
networks: [frontal, trasera] # el único servicio en ambas
# --- Web: página estática y proxy inverso ------------------------------
aurora-web:
<<: *comun
image: nginx:alpine
ports:
- "8080:80" # la ÚNICA puerta desde el host
volumes:
- ./web/index.html:/usr/share/nginx/html/index.html:ro
- ./web/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
aurora-api: { condition: service_healthy }
stop_signal: SIGQUIT # nginx apaga ordenadamente con SIGQUIT
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost/"]
interval: 15s
timeout: 3s
retries: 3
deploy:
resources: { limits: { memory: 128M, cpus: "0.5", pids: 50 } }
networks: [frontal] # sin acceso a los datos
volumes:
aurora-datos:
networks:
frontal:
driver: bridge
trasera:
driver: bridge
internal: true # sin salida a Internet ni entrada desde fueraCiento treinta líneas comentadas que sustituyen a cincuenta líneas de bash y, sobre todo, expresan cosas que el guion no sabía decir: quién depende de quién, qué significa estar listo, y qué puede hablar con qué.
- Puesta en marcha end-to-end
cd ~/aurora-libros
docker compose down -v # partimos de cero, es un entorno de pruebas
docker compose up -d --build --wait[+] Running 8/8
✔ Network aurora-libros_frontal Created
✔ Network aurora-libros_trasera Created
✔ Volume "aurora-libros_aurora-datos" Created
✔ Container aurora-libros-aurora-db-1 Healthy
✔ Container aurora-libros-aurora-cache-1 Healthy
✔ Container aurora-libros-aurora-migraciones-1 Exited
✔ Container aurora-libros-aurora-api-1 Healthy
✔ Container aurora-libros-aurora-web-1 HealthyLee la secuencia: las dos redes y el volumen primero, luego base de datos y caché hasta Healthy, después las migraciones hasta Exited con código 0, y solo entonces la API y la web. El orden ya no lo escribes tú: lo deduce Compose del grafo de dependencias.
SERVICE STATUS
aurora-api Up 20 seconds (healthy)
aurora-cache Up 46 seconds (healthy)
aurora-db Up 46 seconds (healthy)
aurora-migraciones Exited (0) 32 seconds ago
aurora-web Up 8 seconds (healthy)La prueba de fuego: la cache-aside en funcionamiento y, después, la resiliencia que exigimos en el apartado 9.
curl -s http://localhost:8080/api/libros | jq '{origen, total: (.libros|length)}'
curl -s http://localhost:8080/api/libros | jq '{origen, total: (.libros|length)}'
docker compose stop aurora-cache
curl -s http://localhost:8080/api/libros | jq '{origen, total: (.libros|length)}'
docker compose start aurora-cachePrimera petición desde PostgreSQL, segunda desde Redis: los nueve títulos atravesando web → api → db → cache exactamente como se diseñó, y http://localhost:8080 en el navegador muestra el catálogo completo. Y con la caché parada, la API sigue sirviendo desde la base de datos: degradación elegante, se pierde velocidad, no servicio.
- El recuento del onboarding
| Momento | Pasos para tener Aurora Libros funcionando |
|---|---|
| Lección 01-07, sin Docker | 15 pasos manuales: Node, PostgreSQL, Redis, nginx, usuarios, esquema... |
Módulo 3, con docker run |
Un guion de 50 líneas que hay que mantener a mano |
| Ahora | 2 comandos |
git clone https://github.com/auroralibros/plataforma.git && cd plataforma
docker compose up -d --waitEso es todo. Alguien que entra hoy en el equipo tiene la plataforma completa —cuatro servicios, dos redes, un volumen, migraciones aplicadas y sondas de salud verificadas— en menos de un minuto, y con exactamente la misma configuración que el resto del equipo, porque está en Git.
Errores Comunes y Consejos
Usar el puerto publicado entre contenedores. DB_HOST=localhost:5432 desde la API no encuentra nada: dentro del contenedor, localhost es él mismo. Nombre de servicio y puerto interno, siempre.
Confiar en depends_on sin condition. Solo garantiza orden de arranque. Sin sondas de salud, seguirás teniendo carreras.
Sondar el puerto en lugar del servicio. nc -z aurora-db 5432 da verde mientras PostgreSQL aún rechaza conexiones. Usa pg_isready.
Olvidar restart: "no" en un servicio de un solo uso. Con una política heredada, la migración se reinicia en bucle y el service_completed_successfully nunca se cumple.
Suponer que depends_on reconecta. No vuelve a evaluarse tras el arranque. La reconexión con reintentos es de la aplicación.
Tratar la caché como dependencia dura. Si tu API muere porque Redis no responde, has convertido un acelerador opcional en un punto único de fallo.
Consejo: cuando dos servicios no se vean, la primera comprobación es siempre docker compose exec <origen> getent hosts <destino>. Si no resuelve, no es un problema de la aplicación: es que no comparten red.
Ejercicios
Ejercicio 1. Añade a aurora-db el alias de red postgres y demuestra que la API puede conectarse indistintamente por aurora-db o por postgres, pero que aurora-web no resuelve ninguno de los dos.
Ejercicio 2. Rompe el arranque a propósito: haz que aurora-migraciones termine con código 1 (por ejemplo, command: ["sh", "-c", "echo 'migración fallida'; exit 1"]) y observa qué hace docker compose up -d --wait con la API y con la web. Explica el resultado y por qué es el comportamiento deseable.
Ejercicio 3. Demuestra que el aislamiento de trasera es real en las dos direcciones: (a) aurora-db no puede salir a Internet, (b) desde otra máquina de tu red local no se puede alcanzar el 5432, y (c) aurora-api sí puede salir a Internet. Explica por qué (c) funciona a pesar de estar en la red interna.
Soluciones
Solución 1.
docker compose up -d
docker compose exec aurora-api getent hosts aurora-db
docker compose exec aurora-api getent hosts postgres
docker compose exec aurora-web getent hosts postgres; echo "código: $?"La misma dirección para los dos nombres: el alias es una entrada DNS adicional en esa red, no otro contenedor. Para aurora-web, código 2 (no resuelve), porque los alias viven dentro de la red en la que se declaran y nginx no está en trasera. Este es el mecanismo con el que se migra un servicio sin tocar el código de sus clientes: primero se añade el alias nuevo, luego se cambia el nombre del servicio.
Solución 2.
docker compose up -d --wait; echo "código de salida: $?"
docker compose ps -a --format "table {{.Service}}\t{{.Status}}"dependency failed to start: container aurora-libros-aurora-migraciones-1 exited (1)
código de salida: 1
SERVICE STATUS
aurora-cache Up 15 seconds (healthy)
aurora-db Up 15 seconds (healthy)
aurora-migraciones Exited (1) 3 seconds agoNi aurora-api ni aurora-web llegan a crearse. Compose corta la cadena: si una dependencia con service_completed_successfully sale con código distinto de 0, todo lo que depende de ella se cancela, y ese fallo se propaga en cascada a la web, que depende de la API.
Es exactamente lo que quieres. La alternativa —arrancar la API contra un esquema a medio migrar— produciría errores intermitentes, datos corruptos y una hora de depuración. Además, --wait devuelve código 1, así que un pipeline de CI se pone en rojo automáticamente en lugar de continuar sobre una base rota. Compara con el guion de bash del módulo 3: allí, si el paso 4 fallaba, los pasos 5, 6 y 7 se ejecutaban igual.
Solución 3.
# (a) la base de datos no sale a Internet
docker compose exec aurora-db ping -c 1 -W 2 1.1.1.1
# (b) el puerto 5432 no está publicado en ninguna interfaz
ss -ltnp | grep 5432 || echo "5432 no publicado en el host"
# (c) la API sí sale
docker compose exec aurora-api wget -qO- -T 3 https://example.com > /dev/null \
&& echo "la API sí tiene salida"(a) y (b) confirman el doble aislamiento: internal: true elimina la ruta por defecto de esa red, y al no haber ports en aurora-db, no existe ninguna regla en el host que redirija hacia ella —ni siquiera desde el propio host, y mucho menos desde otra máquina de la red local—.
(c) funciona porque aurora-api está en las dos redes. Un contenedor conectado a varias redes tiene una interfaz en cada una, y la ruta por defecto se la da frontal, que no es interna. De ahí que la API sea la "frontera": es el único punto por el que la zona de datos se comunica con el mundo, y por tanto el único que hay que vigilar de cerca.
Conclusión
Aurora Libros está montada de verdad. Sabes que los servicios se encuentran por el nombre del servicio —no el del contenedor— gracias al DNS interno de cada red, que los aliases añaden nombres extra dentro de la red donde se declaran, y que entre contenedores siempre se usa el puerto interno: la publicación de ports solo abre una puerta desde el host, y en esta arquitectura hay exactamente una, el 8080 de la web.
Has segmentado la plataforma en dos zonas: una frontal con la web y la API, y una trasera marcada como internal con los datos y las migraciones, y lo has demostrado en las dos direcciones —nginx no resuelve siquiera el nombre de la base de datos, y PostgreSQL no tiene ruta hacia Internet—, con aurora-api como única frontera por estar en ambas redes. Y tienes resuelto el arranque con la distinción que separa un fichero que funciona en tu portátil de uno que funciona siempre: arrancado no es lo mismo que listo. pg_isready y redis-cli ping como sondas honestas, condition: service_healthy para esperar a que lo estén, service_completed_successfully con restart: "no" para la migración que corre una vez, y el reintento con retroceso exponencial y jitter en la aplicación, porque depends_on cubre el arranque y nada más. La caché, tratada como lo que es: un acelerador cuya caída degrada el servicio pero no lo tumba.
El recuento habla solo: de quince pasos manuales a un guion de cincuenta líneas, y de ahí a git clone y docker compose up -d --wait. Pero el fichero todavía tiene las contraseñas escritas en claro, la versión de la imagen fijada a mano y los puertos incrustados: exactamente lo que impide usarlo en más de un entorno. En la lección siguiente, Variables de Entorno en Docker Compose, separarás los dos sistemas que todo el mundo confunde —la sustitución ${VAR} que Compose hace sobre el propio YAML, y las variables que ve el proceso dentro del contenedor—, con su tabla de precedencia completa, el fichero .env del proyecto y su .env.example versionado, y el bloque secrets: para que las credenciales dejen de ser visibles en un docker inspect.
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
