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

  1. La arquitectura final
  2. Cómo se encuentran los servicios: el DNS de Compose
  3. Quién habla con quién y por qué puerto
  4. Segmentación en dos redes
  5. Demostración: la web no llega a la base de datos
  6. El orden de arranque: por qué depends_on a secas no basta
  7. Sondas de salud reales y condition: service_healthy
  8. Un servicio que corre una vez: service_completed_successfully
  9. Por qué la aplicación debe reintentar igualmente
  10. El compose.yaml completo y comentado
  11. Puesta en marcha end-to-end
  12. El recuento del onboarding

  1. 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.

  1. 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.

docker compose exec aurora-api getent hosts aurora-db aurora-cache
172.21.0.3   aurora-db
172.21.0.4   aurora-cache

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.

  1. 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.

  1. 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 aurora-api, aurora-db, aurora-cache, aurora-migraciones Datos, sin salida a Internet
networks:
  frontal:
    driver: bridge
  trasera:
    driver: bridge
    internal: true

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.

  1. 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-db
172.21.0.5   aurora-api

La 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.8
nc: 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 unreachable

Desde 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.

  1. El orden de arranque: por qué depends_on a secas no basta

Prueba lo que pasa sin ninguna condición, con depends_on: [aurora-db] en la API:

docker compose down && docker compose up -d && sleep 2 && docker compose logs aurora-api --tail 3
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 1

Compose 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

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í.

  1. Sondas de salud reales y condition: service_healthy

La 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_healthy

Ahora 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.

  1. Un servicio que corre una vez: service_completed_successfully

Aurora 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:

    depends_on:
      aurora-migraciones:
        condition: service_completed_successfully

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.

  1. 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.

  1. El 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 fuera

Ciento 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é.

  1. 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        Healthy

Lee 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.

docker compose ps --format "table {{.Service}}\t{{.Status}}"
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-cache
{ "origen": "db", "total": 9 }
{ "origen": "cache", "total": 9 }
{ "origen": "db", "total": 9 }

Primera 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.

  1. 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 --wait

Eso 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.

  aurora-db:
    networks:
      trasera:
        aliases: [postgres]
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: $?"
172.21.0.3   aurora-db
172.21.0.3   postgres
código: 2

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 ago

Ni 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"
ping: sendto: Network is unreachable
5432 no publicado en el host
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

Módulo 2: Trabajando con Imágenes Docker

Módulo 3: Contenedores Docker

Módulo 4: Docker Compose

Módulo 5: Conceptos Avanzados de Docker

Módulo 6: Docker en Producción

Módulo 7: Ecosistema y Herramientas de Docker

© Copyright 2026. Todos los derechos reservados