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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:18
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  db-data:

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

  • api llega a db por el hostname db. 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_URL apunta a db:5432 y el nombre se resuelve.
  • depends_on con condition: service_healthy frena api hasta que Postgres responde de verdad. Un depends_on simple 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:

ClaveQué defineA mano se hace con
serviceslos contenedores a ejecutardocker run
volumesvolúmenes con nombre para datos que deben persistirdocker volume create
networksredes entre serviciosdocker 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í:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
services:
  api:
    build: .
    networks: [frontend, backend]
  db:
    image: postgres:18
    networks: [backend]        # not reachable from frontend
  proxy:
    image: caddy:2
    networks: [frontend]

networks:
  frontend:
  backend:

Los comandos que vas a usar de verdad

Ejecuta estos desde el directorio que contiene compose.yaml.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Levanta todo, con los logs en la terminal
docker compose up

# Levanta en segundo plano
docker compose up -d

# Reconstruye las imágenes que tienen sección build y luego levanta
docker compose up --build

# Para y elimina contenedores y redes (los volúmenes con nombre se conservan)
docker compose down

# Igual, y además borra los volúmenes con nombre
docker compose down -v

# Qué está corriendo en este proyecto
docker compose ps

# Sigue los logs de un servicio
docker compose logs -f api

# Comando puntual en un contenedor nuevo
docker compose run --rm api python manage.py migrate

# Shell en un contenedor que ya está corriendo
docker compose exec api bash

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:

1
2
3
4
5
[+] Running 4/4
 ✔ Network app_default    Created
 ✔ Volume "app_db-data"   Created
 ✔ Container app-db-1      Healthy
 ✔ Container app-api-1     Started

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:

1
2
3
4
5
services:
  db:
    image: postgres:18
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
1
2
# .env  — add this file to .gitignore
DB_PASSWORD=secret

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:

1
2
3
4
5
6
7
8
9
services:
  api:
    build: .
  db:
    image: postgres:18
  seed:
    build: .
    command: python manage.py seed_demo_data
    profiles: [tools]

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:

1
version: "3.8"   # delete it

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  api:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: ./requirements.txt

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_on para 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 un healthcheck y condition: service_healthy.
  • Poner container_name. Te impide ejecutar más de una copia del proyecto y rompe docker 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_modules o 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.

Artículos relacionados