Las aplicaciones reales rara vez viven en un solo contenedor. Una web moderna típica combina varias piezas que colaboran: el servicio que atiende las peticiones, una base de datos que guarda la información y, muy a menudo, una caché que acelera las respuestas. En esta lección reuniremos todo lo aprendido en el módulo para construir, paso a paso, una aplicación multi-contenedor completa con Docker Compose: una API web, una base de datos PostgreSQL y una caché Redis. Verás por qué conviene separar responsabilidades en distintos contenedores, cómo se comunican los servicios entre sí simplemente por su nombre, cómo orquestar su orden de arranque y cómo levantar y probar el conjunto de principio a fin.

Objetivos de Aprendizaje

  • Entender por qué las aplicaciones se dividen en varios contenedores.
  • Construir una aplicación con web, base de datos y caché.
  • Comprender cómo se comunican los servicios usando su nombre como nombre de host.
  • Controlar el orden de arranque con depends_on y entender sus límites.
  • Levantar, verificar y probar la aplicación completa de extremo a extremo.

¿Por Qué Aplicaciones Multi-Contenedor?

Podrías sentir la tentación de meter todo (servidor web, base de datos y caché) en un único contenedor. Es una mala idea. La filosofía de Docker recomienda un proceso principal por contenedor, y separar los servicios aporta ventajas claras:

  • Separación de responsabilidades: cada contenedor hace una sola cosa y la hace bien. Más fácil de entender y mantener.
  • Escalabilidad independiente: si necesitas más capacidad web, puedes ejecutar varias réplicas de la API sin duplicar la base de datos.
  • Actualizaciones aisladas: puedes actualizar la versión de Redis sin tocar la base de datos ni la web.
  • Reutilización: la misma imagen oficial de PostgreSQL o Redis sirve para muchos proyectos.
  • Resiliencia: si un servicio falla, los demás pueden seguir funcionando y reiniciarse de forma independiente.
Enfoque Ventajas Inconvenientes
Todo en un contenedor Aparentemente simple Difícil de mantener, escalar y actualizar; frágil
Multi-contenedor Modular, escalable, mantenible Requiere orquestación (y para eso está Compose)

La Arquitectura del Ejemplo

Vamos a construir una aplicación con tres servicios que colaboran:

  • web: una API que atiende las peticiones de los usuarios.
  • db: una base de datos PostgreSQL donde la API guarda y consulta datos.
  • cache: una caché Redis que la API usa para acelerar respuestas frecuentes.

El flujo es: el usuario llama a web, que a su vez habla con db y cache por la red interna que Compose crea automáticamente.

Usuario ──▶ web (API) ──▶ db    (PostgreSQL)
                       └─▶ cache (Redis)

Comunicación entre Servicios por Nombre

Este es uno de los conceptos más potentes y, a la vez, más mágicos para quien empieza. Cuando defines varios servicios en un mismo docker-compose.yml, Compose los conecta a una red común y habilita un DNS interno. Gracias a ello, cada servicio puede encontrar a los demás usando simplemente su nombre de servicio como nombre de host.

  • La API no necesita conocer la dirección IP de la base de datos.
  • Basta con que se conecte al host db y al puerto de PostgreSQL (5432).
  • Compose resuelve db a la IP interna del contenedor correspondiente automáticamente.
# Dentro del contenedor "web", estas conexiones funcionan:
# Base de datos:  host = db     puerto = 5432
# Caché:          host = cache  puerto = 6379

Explicación:

  • db y cache son exactamente los nombres que les diste a los servicios en el YAML.
  • No hace falta publicar (ports) los puertos de db ni de cache hacia el host para que web los use: la comunicación ocurre dentro de la red de Docker. Solo publicamos hacia fuera lo que el usuario debe ver (la API).

Idea clave: el nombre del servicio es el nombre de host. Si llamas a tu base de datos db, tu aplicación se conecta a db; si la llamas postgres, se conecta a postgres. Mantén nombres claros y coherentes.

Orden de Arranque con depends_on

La API no debería intentar usar la base de datos antes de que esta exista. Con depends_on indicamos a Compose que arranque primero db y cache, y luego web.

services:
  web:
    build: ./web
    depends_on:
      - db
      - cache
  • depends_on: - db - cache: Compose arrancará db y cache antes que web, y al apagar, parará web primero.

Recuerda el matiz importante que ya vimos: depends_on garantiza el orden de arranque del contenedor, pero no que la base de datos esté lista para aceptar conexiones. Postgres puede tardar unos segundos en inicializarse. Hay dos formas de gestionarlo:

  • Reintentos en la aplicación (recomendado y robusto): que tu código reintente la conexión a la base de datos durante unos segundos al arrancar.
  • Healthchecks con condition: service_healthy: hacer que web espere a que db informe de que está sana. Esta técnica avanzada la profundizaremos más adelante.

Ejemplo Completo End-to-End

Construyamos la aplicación entera. La estructura del proyecto será:

miapp/
├── docker-compose.yml
└── web/
    └── Dockerfile

Paso 1: El docker-compose.yml

services:
  web:
    build: ./web                 # construye la API desde ./web/Dockerfile
    ports:
      - "8080:8080"              # expone la API en http://localhost:8080
    environment:
      DB_HOST: db                # nombre de host de la base de datos
      DB_PORT: "5432"
      DB_NAME: app_db
      DB_USER: app_user
      DB_PASSWORD: changeme_in_real_env
      CACHE_HOST: cache          # nombre de host de la caché
      CACHE_PORT: "6379"
    depends_on:
      - db
      - cache
    restart: on-failure

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: app_db
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: changeme_in_real_env
    volumes:
      - datos_db:/var/lib/postgresql/data
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    restart: unless-stopped

volumes:
  datos_db:

Explicación detallada:

  • web:
    • build: ./web construye la imagen desde el Dockerfile de la carpeta web.
    • ports: "8080:8080" publica la API hacia tu máquina; es el único servicio visible desde fuera.
    • environment pasa a la aplicación los datos de conexión. Fíjate en que DB_HOST vale db y CACHE_HOST vale cache: los nombres de los servicios. Así la API sabe a qué hosts conectarse dentro de la red.
    • depends_on asegura que db y cache arranquen antes.
    • restart: on-failure la reinicia si falla (por ejemplo, si la base de datos aún no estaba lista y la app no reintentó).
  • db: PostgreSQL con su configuración por variables de entorno y un volumen datos_db para no perder los datos. No publica puertos: solo web la necesita, y por la red interna.
  • cache: Redis ligero, sin puertos publicados, accesible solo desde dentro.
  • volumes: datos_db: declara el volumen con nombre que persiste los datos de la base.

Advertencia de seguridad: changeme_in_real_env es un valor ficticio solo para el ejemplo. En un proyecto real, gestiona las contraseñas con variables de entorno y un gestor de secretos, nunca en texto plano dentro del YAML (lo verás en detalle en la lección de Variables de Entorno).

Paso 2: El Dockerfile de la API

Para que el ejemplo sea autocontenido, usamos una API mínima. Crea web/Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY app.py .
EXPOSE 8080
CMD ["python", "app.py"]

Explicación:

  • FROM python:3.12-slim: imagen base ligera de Python.
  • WORKDIR /app: directorio de trabajo dentro del contenedor.
  • COPY app.py .: copia el código de la aplicación.
  • EXPOSE 8080: documenta que la app escucha en el puerto 8080.
  • CMD ["python", "app.py"]: comando que arranca la API.

(El fichero app.py contendría tu API; aquí nos centramos en la orquestación con Compose, no en el código de la aplicación.)

Paso 3: Levantar la Aplicación

Desde la carpeta miapp/, construye y arranca todo en segundo plano:

docker compose up -d --build
  • --build construye la imagen de web a partir de su Dockerfile.
  • -d deja la aplicación corriendo en segundo plano.
  • Compose crea la red interna, el volumen datos_db y arranca los tres servicios en el orden correcto.

Paso 4: Verificar el Estado

Comprueba que los tres servicios están en marcha:

docker compose ps
  • Deberías ver web, db y cache con estado running.
  • Solo web mostrará un puerto publicado (8080), porque db y cache se comunican únicamente por la red interna.

Revisa los registros para detectar errores de arranque o de conexión:

docker compose logs -f

Paso 5: Probar la Comunicación entre Servicios

Puedes confirmar que web resuelve y alcanza a db y cache por su nombre. Entra al contenedor web y prueba la conectividad:

docker compose exec web sh
# Dentro del contenedor:
#   ping -c 2 db       # responde la IP interna de la base de datos
#   ping -c 2 cache    # responde la IP interna de la caché
  • Si ping db y ping cache resuelven a una IP, el DNS interno de Compose funciona y la comunicación por nombre está operativa.
  • (Algunas imágenes mínimas no incluyen ping; en ese caso, la propia conexión de la aplicación a db:5432 ya confirma que la resolución por nombre funciona.)

Paso 6: Apagar la Aplicación

Cuando termines, detén y limpia el entorno:

docker compose down
  • Elimina los contenedores y la red, pero conserva el volumen datos_db, de modo que los datos seguirán ahí la próxima vez que ejecutes up.

Errores Comunes y Consejos

  • Usar localhost para conectar entre contenedores: dentro de un contenedor, localhost se refiere a ese mismo contenedor, no a los demás. Para llegar a la base de datos usa el nombre del servicio (db), no localhost.
  • Publicar puertos innecesarios: no hace falta ports en db ni en cache si solo se usan internamente. Publica solo lo que el usuario debe ver.
  • Confiar ciegamente en depends_on: garantiza el orden de arranque, no la disponibilidad. Implementa reintentos de conexión en tu aplicación o usa healthchecks.
  • Olvidar --build tras cambiar el código de la API: sin reconstruir, seguirás ejecutando la versión antigua.
  • No persistir la base de datos: sin un volumen con nombre para db, perderás los datos al hacer down. Declara siempre el volumen.
  • Nombres de servicio confusos: como el nombre es también el host, elige nombres claros (db, cache, web) y mantenlos coherentes con la configuración de tu aplicación.

Ejercicios

Ejercicio 1

Explica por qué, dentro del contenedor web, hay que conectar a la base de datos usando el host db en lugar de localhost.

Ejercicio 2

Añade a la aplicación del ejemplo un cuarto servicio llamado adminer (un cliente web para administrar bases de datos) que use la imagen adminer, publique el puerto 8081 del host hacia el 8080 del contenedor y dependa de db.

Ejercicio 3

Tu servicio web falla al arrancar con un error de conexión a la base de datos, aunque depends_on apunta a db. Explica qué está ocurriendo y propón dos formas de solucionarlo.

Soluciones

Solución al Ejercicio 1

Dentro de un contenedor, localhost apunta al propio contenedor, no al host ni a los demás servicios. Como la base de datos corre en otro contenedor distinto, web no la encontraría en su propio localhost. Compose crea una red interna con DNS, de modo que el nombre del servicio (db) se resuelve a la IP del contenedor de la base de datos. Por eso la conexión correcta es al host db (puerto 5432), no a localhost.

Solución al Ejercicio 2

  adminer:
    image: adminer
    ports:
      - "8081:8080"
    depends_on:
      - db
    restart: unless-stopped
  • image: adminer usa la imagen oficial.
  • "8081:8080" publica el puerto 8081 del host hacia el 8080 del contenedor de Adminer.
  • depends_on: - db asegura que la base de datos arranque antes.
  • Una vez levantado, accederás a Adminer en http://localhost:8081 y, dentro de él, te conectarás al servidor usando el host db.

Solución al Ejercicio 3

Lo que ocurre es que depends_on solo garantiza que el contenedor de db haya arrancado, pero no que PostgreSQL ya esté listo para aceptar conexiones. Postgres tarda unos segundos en inicializarse, y durante ese tiempo web intenta conectarse y falla.

Dos formas de solucionarlo:

  1. Reintentos en la aplicación: que el código de web reintente la conexión a db durante unos segundos antes de rendirse. Es la solución más robusta y portable.
  2. Healthcheck con condition: service_healthy: definir un healthcheck en db y hacer que web dependa de ese estado, de modo que Compose no arranque web hasta que la base de datos esté realmente sana.

Como medida adicional, restart: on-failure en web permite que, si falla por este motivo, el contenedor se reinicie y vuelva a intentarlo cuando la base de datos ya esté disponible.

Conclusión

En esta lección has construido una aplicación multi-contenedor completa de extremo a extremo: una API web, una base de datos PostgreSQL y una caché Redis, orquestadas con un único docker-compose.yml. Has entendido por qué conviene separar los servicios en contenedores distintos, cómo se comunican simplemente por su nombre gracias al DNS interno de Compose, cómo controlar el orden de arranque con depends_on (y sus límites), y cómo levantar, verificar, probar y apagar todo el conjunto.

Con esto cierras los fundamentos prácticos del módulo. En la siguiente lección, Variables de Entorno en Docker Compose, aprenderás a parametrizar esta misma aplicación para que un único archivo sirva en desarrollo, pruebas y producción sin reescribir nada, y a manejar los secretos de forma segura en lugar de dejarlos en texto plano como hemos hecho aquí con fines didácticos.

© Copyright 2026. Todos los derechos reservados