Si ya has usado Docker y has visto que una aplicación "de verdad" necesita más de un contenedor —una base de datos, una API, un frontend, una caché—, tarde o temprano te topas con el mismo problema: gestionar todo a base de docker run con sus parámetros se vuelve un caos. Docker Compose es la herramienta que convierte ese caos en un archivo YAML y un solo comando.
En este tutorial vas a aprender qué es Docker Compose, cómo escribir tu primer compose.yaml, cómo orquestar servicios que se comunican entre sí y cómo usar volúmenes y redes para que tus datos no desaparezcan al apagar los contenedores. Todo con ejemplos prácticos que puedes probar en tu propio equipo.
¿Qué es Docker Compose y por qué usarlo?
Docker Compose es una extensión de Docker que permite definir y ejecutar aplicaciones multi-contenedor desde un único archivo de configuración. En lugar de acordarte de los flags de cada docker run, escribes una vez la configuración en YAML y la levantas con un comando.
Piénsalo así: sin Compose, levantar una aplicación web con base de datos y caché implica ejecutar tres comandos manualmente, acordarte de crear una red para que se vean entre sí, montar volúmenes, exponer puertos. Con Compose, todo eso va en un archivo que puedes compartir, versionar y ejecutar en cualquier máquina.
Las ventajas prácticas son inmediatas:
- Un solo comando para levantar todo el stack (
docker compose up) - Configuración declarativa: todo lo que necesita tu aplicación está en un archivo
- Aislamiento: cada servicio corre en su propio contenedor, como recomienda Docker
- Reproducibilidad: el mismo archivo funciona en desarrollo, staging y producción
- Escalabilidad: puedes escalar servicios concretos con
docker compose up --scale
Requisitos para seguir este tutorial
Necesitas tener Docker instalado. Hoy en día hay dos caminos principales:
- Docker Desktop (Windows, Mac, Linux): incluye Docker Engine, Docker CLI y Docker Compose en un solo instalador. Es la opción más cómoda para empezar.
- Docker Engine + Compose plugin (Linux): instalas Docker Engine con tu gestor de paquetes y añades el plugin
docker-composepor separado.
Verifica que todo está listo con estos comandos:
docker --version
docker compose versionSi ambos responden sin error, estás listo. No hace falta ser un experto en Docker, pero conviene tener claros los conceptos básicos: imágenes, contenedores y puertos.
Estructura de un archivo compose.yaml
El corazón de Docker Compose es el archivo compose.yaml (también acepta docker-compose.yml). Ahí describes los servicios que forman tu aplicación y cómo se relacionan.
La sección obligatoria es services. A partir de ahí puedes añadir:
| Sección | Descripción |
|---|---|
services |
Define cada contenedor (API, base de datos, frontend...) |
networks |
Redes personalizadas para la comunicación entre servicios |
volumes |
Volúmenes para persistir datos |
configs |
Archivos de configuración para servicios |
secrets |
Gestión de contraseñas y claves API |
Un compose.yaml mínimo sería algo así:
services:
web:
image: nginx:alpine
ports:
- "8080:80"Con esto, docker compose up -d descarga Nginx y lo arranca exponiendo el puerto 8080. Pero lo interesante de Compose empieza cuando tienes varios servicios.
Ejemplo práctico: aplicación web con Flask + Redis
Vamos a construir un ejemplo real: una aplicación web en Python con Flask que muestra un contador de visitas almacenado en Redis. Todo dentro de contenedores, sin instalar Python ni Redis en tu máquina.
Paso 1: estructura del proyecto
Crea un directorio para el proyecto y dentro coloca estos archivos:
app.py — la aplicación Flask:
import time
import redis
from flask import Flask
app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)
def get_hit_count():
retries = 5
while True:
try:
return cache.incr('hits')
except redis.exceptions.ConnectionError as exc:
if retries == 0:
raise exc
retries -= 1
time.sleep(0.5)
@app.route('/')
def hello():
count = get_hit_count()
return f'Hola desde Docker Compose! Has visitado esta página {count} veces.'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)requirements.txt:
flask==3.1.0
redis==5.2.0Dockerfile:
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]Paso 2: el archivo compose.yaml
En la raíz del proyecto, crea compose.yaml:
services:
web:
build: .
ports:
- "8000:5000"
depends_on:
- redis
redis:
image: redis:7-alpineFíjate en depends_on: le dice a Compose que el servicio web necesita que redis esté disponible antes de arrancar. No es una garantía de salud (Redis puede estar arrancando aún), pero sí ordena la secuencia de inicio.
Paso 3: levantar la aplicación
Ejecuta en el directorio del proyecto:
docker compose up -dDocker Compose construye la imagen de tu aplicación (usando el Dockerfile), descarga la imagen de Redis, crea los contenedores y los arranca en segundo plano. Abre http://localhost:8000 y verás el contador. Cada vez que recargues, se incrementa.
Para ver el estado:
docker compose psPara ver logs en tiempo real:
docker compose logs -fPara parar y limpiar todo:
docker compose downMejorando el desarrollo con bind mounts
En el ejemplo anterior, cada vez que modificas app.py tienes que reconstruir la imagen y reiniciar el contenedor. En desarrollo eso es tedioso. La solución son los bind mounts: montar tu directorio local dentro del contenedor para que los cambios se reflejen al instante.
Modifica el servicio web en compose.yaml:
services:
web:
build: .
ports:
- "8000:5000"
volumes:
- .:/app
depends_on:
- redisAhora cualquier cambio que hagas en app.py se ve reflejado dentro del contenedor. Si además arrancas Flask en modo debug (FLASK_DEBUG=1 como variable de entorno), el servidor se recarga automáticamente al guardar el archivo.
Docker Compose Watch va un paso más allá: con docker compose up --watch, Compose vigila los archivos del proyecto y sincroniza los cambios sin necesidad de bind mounts explícitos. Cuando guardas un archivo, lo copia al contenedor y reinicia el proceso si es necesario.
Redes y volúmenes
Redes
Por defecto, Compose crea una red para tu proyecto y todos los servicios se conectan a ella. Pueden comunicarse usando el nombre del servicio como hostname. Por eso en app.py usamos redis como host: Docker resuelve ese nombre automáticamente al contenedor de Redis.
Puedes definir redes personalizadas para tener más control:
services:
web:
build: .
networks:
- frontend
- backend
redis:
image: redis:7-alpine
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridgeAquí web está en dos redes, pero redis solo en backend. Si añadieras una base de datos, podría estar solo en backend y no exponerse a frontend. Esto reduce la superficie de ataque y mantiene los servicios separados por función.
Volúmenes
Los contenedores son efímeros: cuando los eliminas, todo lo que hay dentro desaparece. Para los datos que deben persistir —bases de datos, archivos subidos, configuraciones— usamos volúmenes.
Un ejemplo con MongoDB:
services:
database:
image: mongo:7
volumes:
- mongodb_data:/data/db
volumes:
mongodb_data:El volumen mongodb_data se define en la sección volumes y se monta en /data/db, que es donde Mongo guarda sus datos. Aunque elimines y recrees el contenedor, los datos sobreviven en el volumen gestionado por Docker.
Comandos esenciales de Docker Compose
En el día a día, con unos pocos comandos tienes cubierto casi todo el flujo de trabajo:
| Comando | Qué hace |
|---|---|
docker compose up -d |
Construye imágenes (si es necesario) y levanta todos los servicios en segundo plano |
docker compose down |
Detiene y elimina contenedores, redes y (opcionalmente) volúmenes |
docker compose ps |
Muestra el estado de los servicios |
docker compose logs -f |
Sigue los logs de todos los servicios en tiempo real |
docker compose logs -f web |
Logs de un servicio concreto |
docker compose build |
Reconstruye las imágenes sin arrancar los servicios |
docker compose exec web sh |
Abre una shell dentro del contenedor web |
docker compose restart |
Reinicia todos los servicios |
Un tip: si ves que un servicio no arranca bien, prueba docker compose logs -f <servicio> para ver exactamente qué está fallando. Nueve de cada diez veces es un error de configuración o de conexión entre servicios.
Errores comunes (y cómo evitarlos)
Olvidar el .dockerignore: sin este archivo, COPY . . incluye node_modules, .env y otros archivos que no deberían estar en la imagen. Crea siempre un .dockerignore:
node_modules
.git
.env
__pycache__
*.logDependencias sin orden real: depends_on asegura el orden de arranque, pero no espera a que el servicio esté listo. Redis puede arrancar en 1 segundo, pero una base de datos puede tardar 20. En producción usa herramientas como healthcheck o scripts de espera.
Puertos en conflicto: si dos stacks usan el mismo puerto del host, el segundo falla. Planifica los puertos o usa rangos dinámicos.
Olvidar los volúmenes: es el error más común con bases de datos. Arrancas un contenedor de PostgreSQL, creas tablas, haces docker compose down, y al levantar otra vez... todo ha desaparecido. Solución: define volúmenes para cualquier dato que quieras conservar.
Usar latest sin control: las etiquetas latest cambian con el tiempo y pueden romper tu stack. Prefiere versiones concretas: redis:7-alpine en lugar de redis:latest.
Docker Compose es una de esas herramientas que, una vez que las usas, te preguntas cómo trabajabas sin ella. Te permite definir entornos completos en un archivo YAML, compartirlos con tu equipo, y pasar de desarrollo a producción con la misma configuración. Si ya usas Docker, es el siguiente paso natural. Si no, este tutorial es una buena excusa para empezar.
Ahora toca ponerlo en práctica. Crea un compose.yaml para tu próximo proyecto, aunque sea pequeño, y verás cómo cambia la forma en que trabajas con contenedores.