El problema que resuelve Compose
Una aplicación real casi nunca es un solo contenedor. Una API web necesita una base de datos, la base de datos necesita un volumen para que sus datos sobrevivan a un reinicio, y luego añades una caché Redis y un worker en segundo plano. Son cuatro comandos docker run, cada uno con sus flags para puertos, volúmenes, variables de entorno y una red compartida. En el orden correcto. Cada vez que te pones a trabajar.
Docker Compose reemplaza esos comandos por un archivo y un comando. Describes los contenedores y cómo se conectan en un archivo llamado compose.yaml, y docker compose up los levanta todos. Si nunca has usado imágenes ni docker run, lee primero qué es Docker y cómo funcionan los contenedores.
Qué describe el archivo compose
Un archivo compose tiene unas pocas claves de primer nivel. La que siempre usas es services: cada servicio es un contenedor que Compose va a ejecutar.
| |
Dos contenedores. api se construye desde el Dockerfile del directorio actual y publica el puerto 8000. db ejecuta la imagen oficial postgres:18 y guarda sus datos en un volumen con nombre, así los datos siguen ahí cuando el contenedor se recrea.
Dos detalles son los que de verdad importan aquí:
apillega adbpor el hostnamedb. Compose pone cada servicio en una red compartida y registra el nombre de cada servicio como nombre DNS. Nada de direcciones IP, nada del antiguo--link.DATABASE_URLapunta adb:5432y el nombre se resuelve.depends_onconcondition: service_healthyfrenaapihasta que Postgres responde de verdad. Undepends_onsimple solo espera a que el contenedor arranque, no a que el proceso de la base de datos que hay dentro acepte conexiones: esa diferencia es la causa más común de un “connection refused” en el primer arranque.
Las tres claves de primer nivel: services, volumes, networks
Tres claves de primer nivel se corresponden directamente con conceptos de Docker que ya conoces:
| Clave | Qué define | A mano se hace con |
|---|---|---|
services | los contenedores a ejecutar | docker run |
volumes | volúmenes con nombre para datos que deben persistir | docker volume create |
networks | redes entre servicios | docker network create |
networks casi nunca lo declaras. Compose crea una red por proyecto y conecta a ella cada servicio, y por eso el ejemplo con Postgres no necesitó ninguna configuración de red. Declara redes de forma explícita solo cuando quieras impedir que ciertos grupos de servicios se comuniquen entre sí:
| |
Los comandos que vas a usar de verdad
Ejecuta estos desde el directorio que contiene compose.yaml.
| |
up y down son el par que más escribes. up se puede relanzar sin problema: después de editar el archivo, recrea solo los servicios cuya configuración cambió y deja el resto en paz.
Qué imprime docker compose up en el primer arranque
Ejecuta el ejemplo con Postgres de arriba y obtienes algo parecido a esto:
| |
Compose antepone a cada nombre el del proyecto (por defecto, el del directorio) y le añade un número al final, porque está listo para ejecutar más de una réplica del mismo servicio. app-db-1 aparece como Healthy antes de que app-api-1 arranque: es condition: service_healthy haciendo su trabajo.
Cómo mantener las contraseñas fuera del archivo compose
Compose lee un archivo llamado .env en el directorio del proyecto y sustituye las referencias ${VAR} del archivo compose:
| |
| |
Mantén .env fuera del control de versiones y versiona en su lugar un .env.example con valores vacíos o de mentira. Para algo realmente sensible en un entorno desplegado, usa Docker secrets en vez de variables de entorno.
Servicios opcionales con profiles
No todos los servicios tienen que arrancar siempre. Una entrada profiles mantiene un servicio inactivo hasta que lo pides:
| |
docker compose up arranca api y db e ignora seed por completo. docker compose --profile tools run --rm seed ejecuta el seeder cuando lo pides. Usa profiles para todo lo que sea puntual: seeders, migraciones, una shell de depuración, un generador de carga que solo quieres durante una prueba.
Compose v2 vs el viejo docker-compose
Si una guía te dice que ejecutes docker-compose con guion, es anterior a 2023. Esa era la v1, escrita en Python, ya sin soporte. La herramienta actual es docker compose como subcomando de la CLI de Docker; se incluye con Docker Desktop y con el paquete del plugin Compose para Docker Engine.
Los archivos antiguos suelen llevar esta línea al principio:
| |
La clave version no tiene ningún efecto en Compose v2. Compose valida contra la Compose Specification actual y te avisa cuando la clave está presente. Quítala y el aviso desaparece.
El nombre por defecto del archivo también cambió: compose.yaml es el actual, docker-compose.yml sigue funcionando, y Compose busca ambos.
Recarga en vivo durante el desarrollo
docker compose watch (Compose 2.22 en adelante) actualiza los contenedores mientras editas. Añade un bloque develop al servicio:
| |
sync copia los archivos modificados directamente al contenedor en ejecución, así un cambio de código se ve sin reconstruir. rebuild dispara una reconstrucción completa de la imagen cuando cambia un archivo que afecta a la build, como un lockfile de dependencias. Arráncalo con docker compose watch, o añade --watch a up.
Cuándo Compose es la herramienta equivocada
Compose ejecuta contenedores en una sola máquina. Ese es el límite, y casi todo lo que la gente echa en falta en Compose está al otro lado.
Sirve para desarrollo local, pruebas automáticas en CI y despliegues pequeños en un solo host. No sabe planificar contenedores entre varios servidores, reemplazar uno cuando un nodo se cae, hacer despliegues progresivos condicionados a los healthchecks, ni autoescalar.
Eso es trabajo de un orquestador: Kubernetes, o Docker Swarm si quieres algo más ligero. Los dos no compiten. Un archivo compose suele ser el borrador que luego se convierte en un conjunto de manifiestos de Kubernetes, y muchos equipos siguen usando Compose en local mucho después de que producción haya pasado a un cluster.
Con Podman también hay una opción compatible: podman compose y podman-compose leen el mismo formato de archivo, con los matices que se ven en Docker vs Podman.
Errores comunes
- Secretos escritos en
compose.yaml. El archivo acaba en Git. Usa.env(en gitignore) o Docker secrets. - Fiarte de
depends_onpara esperar a que un servicio esté listo. Por sí solo espera a que el contenedor arranque, no a que el servicio acepte conexiones. Combínalo con unhealthcheckycondition: service_healthy. - Poner
container_name. Te impide ejecutar más de una copia del proyecto y rompedocker compose up --scale. Deja que Compose nombre los contenedores. - Hacer bind mount encima de un directorio de dependencias instaladas. Montar la carpeta del proyecto en un contenedor Node o Python puede tapar el
node_moduleso el virtualenv creado durante la build. Monta subcarpetas del código, o pon un volumen anónimo en la ruta de las dependencias.
Cómo pasar una app existente a Compose
Coge la app que hoy arrancas con un script lleno de líneas docker run y pásala a un compose.yaml un servicio cada vez. Levántala, lee los logs, arregla lo que se rompa, añade el siguiente servicio. Dale un healthcheck a todo aquello de lo que dependan otros servicios: es el paso que se suele saltar, y es el que corta el “connection refused” en un arranque en frío.
Sabrás que funcionó cuando docker compose down -v && docker compose up reconstruya el entorno entero desde cero y la app vuelva igual todas las veces.