#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
- Inicio y apagado con dependencias: alcance de
depends_ony sus condiciones. - Referencia de servicios:
healthcheck, comandos y políticas. - Interpolación de variables: expansión en Compose y valores por defecto.
- Fusión de archivos Compose: reglas de combinación.