Qué es Docker Compose
En el módulo anterior subiste un contenedor a la vez, manualmente, con docker run. Funciona para un contenedor. Pero ¿qué pasa cuando tu sitio necesita tres al mismo tiempo: la app, la base de datos y una caché? ¿Escribir tres comandos enormes cada vez, recordar cada puerto, cada volumen y cada conexión? Olvídalo. El Docker Compose resuelve eso: describes todo en un archivo y subes toda la stack con un solo comando.
🧠 Analogía: el director de la orquesta
Imagina una orquesta. Cada músico (contenedor) sabe tocar su instrumento por su cuenta: la app toca, la base de datos toca, la caché toca. Pero si cada uno empieza cuando quiere, el resultado es ruido. El maestro (Compose) lee la partitura (el archivo docker-compose.yml) y los rige a todos juntos: quién empieza primero, quién habla con quién y en qué tono.
- •A partitura = el archivo
docker-compose.yml - •Los músicos = cada servicio (app, base de datos, caché)
- •O "empezar a sonar" =
docker compose up
💡 Docker Compose ya viene incluido
Si instalaste Docker moderno (Docker Desktop o el Docker Engine reciente en Linux), Compose ya está instalado como un subcomando: lo llamas docker compose (con espacio). En máquinas antiguas, el comando era docker-compose (con guion). Ambos hacen lo mismo; en este módulo usamos la forma nueva con espacio.
# Comprobar si Compose está disponible
$ docker compose version
Docker Compose version v2.27.0
El archivo docker-compose.yml básico
Toda la magia está en un único archivo de texto llamado docker-compose.yml. Está escrito en YAML, un formato sencillo basado en indentación (espacios al comienzo de la línea). La regla de oro: la indentación indica qué está dentro de qué. Vamos de lo más sencillo a lo más completo.
El compose más pequeño posible: un servicio
# docker-compose.yml
services:
app:
image: nginx:alpine
ports:
- "8080:80"
Esto describe un servicio llamado app, que usa la imagen nginx:alpine y expone el puerto 80 del container en el puerto 8080 de tu máquina.
services
La lista de todos los containers del stack. Cada clave aquí dentro es un servicio.
image
La imagen lista para usar (de Docker Hub). Alternativa: build: para construir a partir de tu Dockerfile.
ports
Mapea los puertos en el formato "host:container". La de la izquierda y la de tu PC.
💡 Sobre el "version:" en la parte superior
En los tutoriales antiguos verás una línea version: "3.8" al principio del archivo. En Compose moderno (v2), esta línea es opcional y se ignora: puedes empezar directamente en services:. Si un archivo de ejemplo tiene el version:, no te preocupes, sigue funcionando.
⚠️ Error común
Problema: "yaml: line 4: mapping values are not allowed in this context" o el stack no se inicia.
Solución: Casi siempre es indentación incorrecta. YAML no acepta Tab, solo espacios. Mantén siempre 2 espacios por nivel y nunca mezcles Tab con espacios. En VS Code, activa «mostrar espacios en blanco» para ver el problema.
✓ Qué HACER
- ✓Usar siempre 2 espacios por nivel de indentación
- ✓Poner versiones fijas en las imágenes (
nginx:1.27) - ✓Dar nombres claros a los servicios (
app,banco)
✗ Qué NO hacer
- ✗Usar Tab para indentar (rompe el YAML)
- ✗Usar solo
:latesten producción (se vuelve una sorpresa) - ✗Olvidar las comillas en
"8080:80"
Servicios, redes y volúmenes
Tres conceptos resuelven el 90% de los casos: servicios (los contenedores), redes (cómo se comunican) y volúmenes (dónde se guardan los datos). La gran ventaja de Compose: crea una red automática para tu stack y, dentro de ella, un servicio llama al otro por su nombre.
👁 Qué vas a ver: comunicación por nombre
No usas una IP. Si el servicio de la base de datos se llama banco, la app se conecta a él mediante la URL, usando el nombre del servicio como dirección:
# Dentro del contenedor de la app, la conexión con la base de datos es así:
DATABASE_URL=postgres://user:senha@base de datos:5432/meubanco
# "banco" es el nombre del servicio, NO una IP. Compose lo resuelve.
Red: por defecto, ya está lista
Al iniciar la stack, Compose crea una red solo para ella. Todos los servicios se unen a esa red automáticamente y se reconocen por su nombre. Solo declaras redes manualmente cuando quieres aislar grupos (por ejemplo: separar el frontend del backend).
services:
app:
image: mi-app
depends_on:
- banco # garantiza que la base de datos se inicie antes
Volumen: para que los datos no desaparezcan
El contenedor es desechable: si eliminas el contenedor de la base de datos, los datos se van con él. El volumen guarda los datos fuera del contenedor, en un lugar que sobrevive a su recreación. Decláralo en la raíz y úsalo dentro del servicio:
services:
base de datos:
image: postgres:16
volúmenes:
- datos-db:/var/lib/postgresql/data
# Declaración del volumen en la raíz del archivo:
volúmenes:
datos-db:
Formato: nome-do-volume:/caminho/dentro/do/container. Aunque elimines y vuelvas a crear el contenedor, los datos siguen ahí.
⚠️ Error común
Problema: "Cada vez que ejecuto docker compose down mi base de datos se reinicia."
Solución: Faltó el volumen. Sin un volumen, los datos quedan dentro del contenedor y desaparecen cuando lo eliminas. Declara un volumen con nombre para la base de datos. Ten cuidado también con down -v: o -v borra los volúmenes intencionalmente.
Subir un stack completo: app + base de datos
Llegó la hora de unirlo todo. Vamos a montar una stack real: un app web que se comunica con un base de datos PostgreSQL, con un volumen para que los datos no desaparezcan. Un archivo, un comando, dos contenedores comunicándose.
El docker-compose.yml completo
# docker-compose.yml
services:
app:
build: . # construye a partir del Dockerfile local
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:senha@base de datos:5432/app
depends_on:
- base de datos
base de datos:
image: postgres:16
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=senha
- POSTGRES_DB=app
volúmenes:
- datos-db:/var/lib/postgresql/data
volúmenes:
datos-db:
Subir todo el stack
O -d ("detached") deja los contenedores ejecutándose en segundo plano
$ docker compose up -d
[+] Running 3/3
✓ Network meuapp_default Creada
✓ Container meuapp-banco-1 Iniciado
✓ Container meuapp-app-1 Iniciado
Verificar que se haya subido
Lista los contenedores del stack y el estado de cada uno
$ docker compose ps
NAME SERVICE STATUS PORTS
meuapp-app-1 app Up 12 seconds 0.0.0.0:3000->3000/tcp
meuapp-banco-1 banco Up 13 seconds 5432/tcp
Detener cuando termines
Detiene y elimina los contenedores y la red (pero mantiene el volumen de datos)
$ docker compose down
[+] Running 3/3
✓ Container meuapp-app-1 Removed
✓ Container meuapp-banco-1 Eliminado
✓ Network meuapp_default Eliminada
💡 Consejo: up sin -d para verlo todo de una vez
La primera vez, ejecuta docker compose up sin o -d. Los registros de todos los servicios aparecen mezclados en pantalla, en tiempo real. Es ideal para ver si algo dio error al iniciarse. Cuando todo esté bien, detén todo con Ctrl+C y vuelve a subir con -d.
Logs y depuración: cuando algo falla
¿Subiste el stack y el sitio no abre? Tranquilo, es normal. Compose te da tres herramientas para investigar: ver los registros (lo que dice cada servicio), ver el estado de los contenedores, y entrar de un contenedor para verlo de cerca.
logs - Leer lo que dicen los servicios
# Logs de todos los servicios, siguiéndolos en vivo (-f = follow)
$ docker compose logs -f
banco-1 | database system is ready to accept connections
app-1 | Server listening on port 3000
# Logs de UN solo servicio
$ docker compose logs app
# Solo las últimas 50 líneas
$ docker compose logs --tail 50 banco
El log es el primer lugar que debes revisar. 9 de cada 10 problemas aparecen aquí en texto claro.
ps - Ver el estado de cada contenedor
$ docker compose ps
NAME SERVICE STATUS PORTS
meuapp-app-1 app Exited (1) 4 seconds ago
meuapp-banco-1 banco Up 20 seconds 5432/tcp
Aquí el app está como Exited (1): se cayó. El siguiente paso es revisar su registro con docker compose logs app.
🔎 exec - Entrar en un contenedor
A veces necesitas mirar por dentro: ejecutar un comando, abrir el cliente de la base de datos, revisar un archivo. El exec ejecuta un comando dentro de un contenedor que ya está en marcha:
# Abrir una terminal dentro del contenedor de la app
$ docker compose exec app sh
/app # ls
node_modules package.json server.js
# Abrir el cliente de PostgreSQL dentro de la base de datos
$ docker compose exec banco psql -U user -d app
app=# \dt
⚠️ Error común
Problema: "La app inicia, pero no se conecta a la base de datos: connection refused."
Solución: O depends_on garantiza la orden de subida, pero no espera a que la base de datos quede listo para aceptar conexiones (tarda algunos segundos). Solución simple: haz que la app intente reconectarse algunas veces. Solución robusta: usa healthcheck en la base de datos + condition: service_healthy en depends_on.
Actualización y rollback
Tu aplicación está en línea y quieres publicar una nueva versión. O peor: ya la publicaste y algo salió mal, necesitas volver rápido. Con Compose, actualizar y volver a una versión anterior requiere pocos comandos. El secreto es fijar la etiqueta de la imagen para saber siempre qué versión está en ejecución.
Actualizar a una nueva versión
Supón que tu imagen en el archivo era meu-app:1.4 y es la nueva versión y la 1.5. Cambias la etiqueta en el archivo y actualizas solo lo que cambió:
# 1. Editar el compose: image: meu-app:1.4 -> image: meu-app:1.5
# 2. Descargar la nueva imagen del registry
$ docker compose pull
✓ app Descargada
# 3. Recrear solo los contenedores que cambiaron
$ docker compose up -d
[+] Running 2/2
✓ Container meuapp-banco-1 En ejecución
✓ Container meuapp-app-1 Recreated
Observa: la base de datos queda Running (no se modificó) y solo la app y Recreated. Compose solo modifica lo que cambió.
Rollback: volver a la versión anterior
¿Algo salió mal en la 1.5? Vuelve la etiqueta a 1.4 y vuelve a subir. Como fijaste la versión, sabes exactamente a cuál volver.
# Editar el compose para volver atrás: image: meu-app:1.5 -> image: meu-app:1.4
$ docker compose up -d
✓ Container meuapp-app-1 Recreated
# Listo: en segundos vuelves a la versión 1.4
✓ Qué HACER
- ✓Fijar etiquetas de versión (
app:1.4) para poder volver - ✓Ejecutar
pullantes deup -d - ✓Guardar el compose en Git (cada versión se convierte en historial)
✗ Qué NO hacer
- ✗Usar
:latest(no se puede saber a qué versión volver) - ✗Ejecutar
down -vpensando que solo hay que reiniciar - ✗Actualizar directamente en producción sin probar antes
🏆 Ya puedes ejecutar una stack de verdad
Con la app, la base de datos y el volumen en un archivo, todo iniciándose con un comando, leyendo logs, entrando en los contenedores y volviendo a una versión anterior cuando hace falta, ya tienes lo esencial de la orquestación local. En el próximo módulo aprenderás a hacer que esta stack iniciarse sola cuando se enciende el servidor, con el systemd.
📚 Resumen del módulo
Siguiente módulo:
4.3 - Systemd: hacer que tu stack se inicie sola cuando arranca el servidor y se reinicie si se cae