Cuando desarrollas una aplicación real necesitas varios servicios funcionando a la vez: el servidor web, la base de datos, una caché, quizás un gestor de colas. Levantar cada contenedor manualmente con docker run es tedioso y difícil de reproducir. Docker Compose soluciona esto: describes todos los servicios en un archivo YAML y los levanta todos con un solo comando, con la red configurada automáticamente entre ellos.
Instalación y conceptos básicos
# Docker Compose viene incluido con Docker Desktop (Mac y Windows)
# En Linux, se instala junto con Docker Engine:
# https://docs.docker.com/compose/install/
# Verificar la instalación
docker compose version
# Docker Compose version v2.29.0
# Nota: la versión antigua era "docker-compose" (con guión)
# La versión moderna es "docker compose" (como plugin de Docker)
# Ambas son compatibles, pero la nueva es la recomendada
# Estructura de un proyecto con Docker Compose:
mi-proyecto/
├── docker-compose.yml # o compose.yml (nombre moderno)
├── backend/
│ ├── Dockerfile
│ └── src/
├── frontend/
│ ├── Dockerfile
│ └── src/
└── nginx/
└── nginx.conf
El archivo docker-compose.yml
Todo el stack se describe en un único archivo YAML. Cada bloque bajo services es un contenedor.
# docker-compose.yml básico: aplicación web + base de datos
services:
# Servicio de base de datos
db:
image: postgres:16-alpine # imagen oficial de PostgreSQL
restart: unless-stopped # reiniciar si falla, excepto si lo paras manualmente
environment:
POSTGRES_DB: mi_app
POSTGRES_USER: usuario
POSTGRES_PASSWORD: contraseña_segura
volumes:
- datos_postgres:/var/lib/postgresql/data # persistir los datos
ports:
- "5432:5432" # exponer al host (útil para conectar con un cliente SQL local)
# Servicio de la aplicación backend
backend:
build: ./backend # construir imagen desde el Dockerfile en ./backend
restart: unless-stopped
environment:
DATABASE_URL: postgresql://usuario:contraseña_segura@db:5432/mi_app
# ^^
# nombre del servicio de BD (no "localhost")
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy # esperar a que la BD esté lista
volumes:
- ./backend:/app # montar el código local (útil en desarrollo)
volumes:
datos_postgres: # volumen con nombre: persiste entre reinicios de contenedores
# Comandos fundamentales de Docker Compose
# Levantar todos los servicios (en segundo plano)
docker compose up -d
# Levantar y reconstruir las imágenes si cambiaron
docker compose up -d --build
# Ver el estado de los servicios
docker compose ps
# Ver los logs de todos los servicios
docker compose logs
# Ver los logs de un servicio concreto (y seguirlos en tiempo real)
docker compose logs -f backend
# Parar los servicios (sin borrar contenedores ni volúmenes)
docker compose stop
# Parar y borrar los contenedores (los volúmenes quedan intactos)
docker compose down
# Parar y borrar TODO incluyendo volúmenes (¡borra los datos de la BD!)
docker compose down -v
# Ejecutar un comando en un servicio en ejecución
docker compose exec backend bash
docker compose exec db psql -U usuario -d mi_app
Ejemplo completo: stack web moderno
Una aplicación real con frontend en React, backend en FastAPI, PostgreSQL, Redis y Nginx como proxy inverso.
# docker-compose.yml
services:
# ── Base de datos ──────────────────────────────────────────────────────
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-mi_app}
POSTGRES_USER: ${DB_USER:-usuario}
POSTGRES_PASSWORD: ${DB_PASSWORD:?La contraseña de BD es obligatoria}
volumes:
- datos_postgres:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql # script de inicialización
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-usuario} -d ${DB_NAME:-mi_app}"]
interval: 10s
timeout: 5s
retries: 5
# ── Caché ──────────────────────────────────────────────────────────────
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD:-redis_pass}
volumes:
- datos_redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# ── Backend (FastAPI) ──────────────────────────────────────────────────
backend:
build:
context: ./backend
dockerfile: Dockerfile
restart: unless-stopped
environment:
DATABASE_URL: postgresql://${DB_USER:-usuario}:${DB_PASSWORD}@db:5432/${DB_NAME:-mi_app}
REDIS_URL: redis://:${REDIS_PASSWORD:-redis_pass}@redis:6379/0
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY es obligatoria}
DEBUG: ${DEBUG:-false}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./backend:/app # hot reload en desarrollo
- archivos_subidos:/app/uploads
expose:
- "8000" # solo accesible internamente, no al host
# ── Frontend (React, en desarrollo) ───────────────────────────────────
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev # Dockerfile específico para desarrollo
restart: unless-stopped
environment:
VITE_API_URL: http://localhost/api
volumes:
- ./frontend:/app
- /app/node_modules # excluir node_modules del montaje
expose:
- "5173"
# ── Proxy inverso (Nginx) ──────────────────────────────────────────────
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/certs:/etc/nginx/certs:ro
depends_on:
- backend
- frontend
volumes:
datos_postgres:
datos_redis:
archivos_subidos:
# nginx/nginx.conf: enrutar /api al backend y el resto al frontend
events { worker_connections 1024; }
http {
upstream backend { server backend:8000; }
upstream frontend { server frontend:5173; }
server {
listen 80;
# Rutas de API al backend
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Todo lo demás al frontend
location / {
proxy_pass http://frontend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; # necesario para hot reload
}
}
}
Variables de entorno y el archivo .env
# Docker Compose lee automáticamente el archivo .env del mismo directorio
# .env
DB_NAME=mi_app
DB_USER=usuario
DB_PASSWORD=contraseña_muy_segura_aqui
REDIS_PASSWORD=redis_pass_segura
SECRET_KEY=clave_secreta_para_jwt_muy_larga
DEBUG=true
# En docker-compose.yml, las referencias con ${VARIABLE} se sustituyen
# ${VARIABLE:-default} → usa "default" si VARIABLE no está definida
# ${VARIABLE:?mensaje} → falla con "mensaje" si VARIABLE no está definida
# ${VARIABLE:+valor} → usa "valor" solo si VARIABLE SÍ está definida
# ⚠️ El .env NUNCA debe subirse al repositorio
# Añadirlo al .gitignore:
echo ".env" >> .gitignore
# Proporcionar un .env.example con las claves sin valores reales:
# .env.example (sí va al repositorio)
DB_NAME=mi_app
DB_USER=usuario
DB_PASSWORD= # ← rellenar con la contraseña real
REDIS_PASSWORD= # ← rellenar
SECRET_KEY= # ← generar con: python -c "import secrets; print(secrets.token_hex(32))"
DEBUG=false
Perfiles: configuraciones para dev y producción
# Los profiles permiten activar servicios solo en ciertos entornos
services:
db:
image: postgres:16-alpine
# sin profile: siempre activo
backend:
build: ./backend
# sin profile: siempre activo
# Solo en desarrollo: herramienta de administración de BD
adminer:
image: adminer
ports:
- "8080:8080"
profiles:
- dev # solo se levanta con --profile dev
# Solo en producción: agente de monitorización
datadog:
image: datadog/agent
environment:
DD_API_KEY: ${DD_API_KEY}
profiles:
- prod # solo se levanta con --profile prod
# Uso:
docker compose --profile dev up -d # dev: levanta db + backend + adminer
docker compose --profile prod up -d # prod: levanta db + backend + datadog
docker compose up -d # solo los servicios sin profile: db + backend
Compose para desarrollo vs producción
# Estrategia habitual: un archivo base + un override por entorno
# docker-compose.yml (base, compartido)
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- datos_postgres:/var/lib/postgresql/data
backend:
image: mi-empresa/backend:${VERSION:-latest}
environment:
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
depends_on:
db:
condition: service_healthy
volumes:
datos_postgres:
# docker-compose.override.yml (desarrollo, se aplica automáticamente)
# Docker Compose fusiona este archivo con el base cuando no se especifica otro
services:
backend:
build: ./backend # en dev construimos la imagen localmente
volumes:
- ./backend:/app # hot reload
environment:
DEBUG: "true"
ports:
- "8000:8000" # exponer al host para depurar
db:
ports:
- "5432:5432" # exponer la BD al host para usar un cliente SQL
# docker-compose.prod.yml (producción, especificar manualmente)
services:
backend:
deploy:
replicas: 3 # varias réplicas en producción
resources:
limits:
cpus: "1.0"
memory: 512M
restart: always
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Levantar con el archivo de producción:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Healthchecks y dependencias
# El problema sin healthchecks:
# depends_on solo espera a que el contenedor ARRANQUE,
# no a que el servicio dentro esté LISTO para recibir conexiones.
# PostgreSQL tarda unos segundos en inicializarse después de arrancar.
# Solución: healthcheck + condition: service_healthy
services:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U usuario -d mi_app"]
interval: 10s # frecuencia de comprobación
timeout: 5s # tiempo máximo para considerar fallido
retries: 5 # intentos antes de marcar como unhealthy
start_period: 30s # tiempo de gracia al arrancar (no cuenta como fallo)
backend:
build: ./backend
depends_on:
db:
condition: service_healthy # esperar a que el healthcheck pase
# Healthcheck para Redis:
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
# Healthcheck para una API HTTP:
api:
build: ./api
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
Comandos útiles del día a día
# Ver qué está corriendo y su estado (incluye healthcheck)
docker compose ps
# Ver el uso de recursos de cada contenedor
docker compose stats
# Reconstruir solo un servicio sin reiniciar los demás
docker compose up -d --build backend
# Reiniciar un solo servicio
docker compose restart backend
# Escalar un servicio a N réplicas (si no usa puertos fijos)
docker compose up -d --scale backend=3
# Ejecutar un comando puntual en un servicio (sin que esté en ejecución)
docker compose run --rm backend python manage.py migrate
docker compose run --rm backend pytest
# Copiar un archivo desde/hacia un contenedor
docker compose cp backend:/app/logs/error.log ./logs/
# Ver las variables de entorno que ve un servicio
docker compose exec backend env
# Forzar recrear los contenedores (útil cuando el compose.yml cambia)
docker compose up -d --force-recreate
Resumen
- Docker Compose describe todo el stack de servicios en un archivo YAML y los levanta, conecta y gestiona con un solo comando.
docker compose up -dlevanta todos los servicios en segundo plano;docker compose downlos para y elimina los contenedores. - Los servicios se comunican entre sí usando el nombre del servicio como hostname (por ejemplo,
dboredis), nolocalhost. Docker Compose crea automáticamente una red privada entre todos los servicios del mismo archivo. - Usa variables de entorno con un archivo
.env(que nunca va al repositorio) y proporciona un.env.examplecon las claves sin valores. La sintaxis${VARIABLE:?mensaje}hace que Compose falle con un error claro si falta una variable obligatoria. - Los healthchecks con
condition: service_healthyendepends_ongarantizan que un servicio no arranca hasta que sus dependencias estén realmente listas para recibir conexiones, no solo iniciadas. - La estrategia de archivo base + override (
docker-compose.yml+docker-compose.override.ymlpara dev,docker-compose.prod.ymlpara producción) evita duplicar configuración entre entornos.
No hay comentarios todavía. Sé el primero en compartir tu opinión.