Saltar al contenido

Capítulo 6 de 13

Compose y el arranque de una aplicación

Describir servicios, dependencias y configuración y entender qué garantiza Compose cuando un sistema arranca o falla.

#De órdenes sueltas a una definición repetible

Hasta ahora escribías un docker run por contenedor. Cuando una aplicación tiene proxy, API y base de datos, recordar redes, montajes y variables deja de ser fiable. Docker Compose lee un archivo YAML y crea los recursos que describe. Ese archivo es una explicación ejecutable de cómo se conectan las partes.

Un servicio es la definición de una función: por ejemplo, api. Un contenedor es una instancia de esa definición. Un proyecto de Compose agrupa los recursos de una aplicación y añade un prefijo a sus nombres. Puedes tener dos proyectos con servicios llamados api sin que sean la misma ejecución, siempre que no publiques el mismo puerto del host.

YAML usa sangría para expresar jerarquía. Dos espacios por nivel es una convención legible; no mezcles tabuladores. Una lista comienza con -; un mapa relaciona claves con valores. En la sintaxis de puertos, usa comillas para que la cadena se interprete como tal.

#Primer archivo completo, con una prueba que termina

Guarda compose.yaml en una carpeta de práctica independiente:

services:
  web:
    image: nginx:stable-alpine
    ports:
      - "127.0.0.1:8080:80"
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/"]
      interval: 5s
      timeout: 2s
      retries: 5
      start_period: 5s
  prueba:
    image: python:3.13-slim
    command:
      - python
      - -c
      - "import urllib.request; print(urllib.request.urlopen('http://web').status)"
    depends_on:
      web:
        condition: service_healthy
    restart: "no"

web sirve una página. prueba es un cliente HTTP que terminará tras obtenerla. wget es un cliente de descargas presente en esta variante de Nginx; --spider comprueba la URL sin conservar su contenido. Elegir una herramienta de healthcheck requiere que exista en la imagen: poner curl no lo instala.

docker compose -p compose-practica config --quiet
docker compose -p compose-practica up -d
docker compose -p compose-practica ps -a
docker compose -p compose-practica logs prueba

config --quiet valida configuración e interpolación sin imprimir valores. Espera web healthy y prueba finalizada con código cero, cuyo log muestra 200. Compose crea una red donde web se resuelve por nombre. Al acabar, docker compose -p compose-practica down elimina los contenedores y esa red.

No añadas container_name por costumbre. Compose ya ofrece nombres de servicio para DNS; un nombre de contenedor fijo introduce choques entre proyectos y limita el escalado de ese servicio.

#Orden de inicio no es disponibilidad continua

Un proceso puede existir mientras prepara archivos o acepta todavía cero conexiones. Por eso la forma corta depends_on: [db] expresa orden de arranque, no que la base esté lista para consultas.

Condición Qué espera Compose Uso
service_started Que arranque el contenedor Dependencia sin una señal de disponibilidad definida
service_healthy Que su healthcheck pase Base preparada antes de arrancar API
service_completed_successfully Que la tarea termine con éxito Migración que debe acabar antes de aceptar tráfico

La relación se evalúa al realizar las operaciones de Compose correspondientes. No es un supervisor permanente de todo el grafo. Si la base cae una hora después, la API debe manejar errores de conexión. Tampoco debes confiar en que reiniciar el host reproduce exactamente la secuencia de un compose up: las políticas de reinicio pertenecen al motor.

#Liveness y readiness responden preguntas diferentes

Una señal de liveness pregunta si el proceso sigue funcionando suficientemente como para no reemplazarlo. Una señal de readiness pregunta si está preparado para servir tráfico útil ahora. Si la base se cae, la API puede seguir viva y responder 503 a operaciones que la necesitan.

En una aplicación sencilla puedes tener /health/live que no consulta dependencias y /health/ready que realiza una consulta pequeña con timeout. El healthcheck de Docker puede apuntar a readiness para que Compose espere. Un proxy o monitor necesita su propia política de uso de esa señal: Docker no le comunica automáticamente a todos los componentes que deje de enviar tráfico.

No hagas que una caída compartida de base reinicie compulsivamente todas las APIs: perderías logs y conexiones y añadirías trabajo al sistema saturado. Responder temporalmente 503, limitar tiempos de espera y reconectar puede ser mejor que reiniciar.

#Expresar una dependencia de base de datos

Este fragmento se añade a una aplicación que realmente tenga un servicio api; no es un archivo completo:

services:
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_DB: catalogo
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets: [db_password]
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 15s
  api:
    depends_on:
      db:
        condition: service_healthy
volumes:
  db_data:
secrets:
  db_password:
    file: ./secrets/db_password

$$ evita que Compose expanda la variable en el host y permite que el shell del contenedor lea $POSTGRES_USER. CMD-SHELL usa ese shell; la forma CMD ejecuta directamente una lista. -h 127.0.0.1 comprueba TCP para no confundir el servidor temporal de inicialización, que solo usa socket Unix, con el servidor listo para conexiones de la API. pg_isready indica que PostgreSQL acepta conexiones, pero no prueba que el rol de API tenga permisos ni que exista la tabla. La comprobación de readiness de API debe cubrir su propio contrato.

#Configuración: dónde se expande cada valor

services:
  web:
    image: "${WEB_IMAGE:?Define WEB_IMAGE}"
    environment:
      LOG_LEVEL: "${LOG_LEVEL:-info}"
    ports:
      - "127.0.0.1:${WEB_PORT:-8080}:80"

Compose sustituye ${…} antes de crear los contenedores. :- proporciona un valor por defecto; :? detiene el comando si falta un valor necesario. En un archivo local.env podrías escribir WEB_IMAGE=nginx:stable-alpine y WEB_PORT=8081, y usar docker compose --env-file local.env config.

El archivo .env o --env-file alimenta la interpolación de Compose. No mete todas sus variables automáticamente dentro de cada contenedor. environment y el atributo de servicio env_file sí sirven para configurar el entorno del proceso. Sus prioridades importan cuando repites nombres: reduce duplicación y consulta config localmente para entender el resultado, evitando compartir su salida si contiene secretos.

Un secreto no debería viajar en una variable solo porque resulte cómodo. Usaremos archivos montados por servicio en Seguridad.

#Desarrollo y despliegue necesitan decisiones distintas

En desarrollo, montar código desde el host permite editar y recargar. En despliegue, el código debe venir de la imagen probada para saber qué versión está corriendo. En desarrollo puedes publicar una base en loopback para una herramienta local; en producción quizá no quieras publicarla en absoluto.

Puedes tener archivos completos separados o combinar una base con un override:

docker compose -f compose.yaml -f compose.production.yaml config --quiet

Los archivos no se concatenan como texto: Compose aplica reglas de fusión. Algunas listas se combinan; añadir un nuevo puerto en el override puede dejar también el puerto de desarrollo. Inspecciona siempre la configuración resuelta. Para el proyecto guiado usaremos una configuración completa legible y un cambio explícito al pasar a HTTPS, para que no queden publicaciones ocultas por una fusión.

#Operar sin borrar por reflejo

Comando Efecto Cuándo usarlo
up -d Crea y reconcilia servicios; recrea cuando corresponde Aplicar configuración o imágenes
build api Construye imagen de un servicio Cambió código o receta local
pull api Descarga la imagen configurada Hay un artefacto publicado
restart api Reinicia el contenedor existente Reiniciar proceso con la configuración actual
exec api … Ejecuta algo en la instancia activa Diagnóstico o consulta
run --rm api … Crea una instancia puntual con esa configuración Migración o tarea de administración
stop Para sin eliminar Pausa temporal
down Elimina contenedores y redes de proyecto Desmontar la aplicación

restart no aplica una variable nueva que acabas de editar en YAML; para eso usa up -d y permite recrear. run no publica por defecto los puertos de servicio, evitando choques con la instancia principal. down -v elimina también volúmenes gestionados por el proyecto: no es una manera de resolver errores de arranque con datos importantes.

Cuando una migración es un servicio puntual, haz que tenga restart: "no", un comando que falle con código distinto de cero ante errores y una dependencia de base saludable. La API puede esperar service_completed_successfully. Para entregas repetidas, controla explícitamente cuándo ejecutar cada migración; no hagas que cada worker de API intente cambiar el esquema al arrancar.

#Probar un fallo, no solo un arranque feliz

En una aplicación con API y base, prueba esta secuencia en un entorno desechable: arranca, escribe un dato, detén la base, consulta la API, arranca de nuevo la base y repite. Debes poder explicar la respuesta durante la caída, el tiempo máximo de espera y si se recupera sin perder el dato.

Si el servicio parece sano solo porque un proceso existe, la definición de salud es débil. Si nunca se recupera porque el cliente guardó una conexión rota para siempre, la dependencia declarada en Compose no corrige ese código. El proyecto final realiza ese ensayo con los servicios completos.

#Referencias para profundizar

    ↑ ↓ para recorrer · Intro para abrir · Ctrl K para buscar