Gestionar contenedores Docker individuales con largos comandos docker run se vuelve rápidamente impráctico cuando tu aplicación requiere múltiples servicios interconectados. Docker Compose resuelve este problema permitiéndote definir y gestionar aplicaciones multi-contenedor usando un único archivo YAML declarativo. Para los administradores de sistemas, Docker Compose es una herramienta indispensable que cierra la brecha entre la gestión manual de contenedores y las plataformas de orquestación completas como Kubernetes.

Esta guía cubre todo lo que necesitas saber para usar Docker Compose de manera efectiva en escenarios del mundo real, desde comprender la sintaxis YAML hasta desplegar stacks listos para producción con comprobaciones de salud, límites de recursos y políticas de reinicio adecuadas.

Requisitos Previos

Antes de comenzar, asegúrate de tener:

  • Docker Engine instalado en tu sistema. Si aún no lo has hecho, sigue nuestra guía: Cómo Instalar Docker en Ubuntu 22.04 y 24.04
  • Comprensión básica de los conceptos de Docker (imágenes, contenedores, volúmenes, redes)
  • Un editor de texto y acceso a la terminal con privilegios sudo

¿Qué Es Docker Compose?

Docker Compose es una herramienta para definir y ejecutar aplicaciones Docker multi-contenedor. En lugar de ejecutar múltiples comandos docker run con flags complejos, describes toda tu pila de aplicaciones en un archivo docker-compose.yml (o compose.yml) y luego inicias todo con un solo comando.

Un caso de uso típico podría incluir una aplicación web que depende de una base de datos, una capa de caché y un proxy inverso. Docker Compose te permite definir todos estos servicios, sus configuraciones, redes y volúmenes en un solo lugar, haciendo que tu infraestructura sea reproducible y controlable por versiones.

Compose V2 vs. V1: Qué Cambió

Docker Compose ha pasado por una evolución significativa:

CaracterísticaCompose V1 (Legacy)Compose V2 (Actual)
Comandodocker-compose (binario separado)docker compose (plugin CLI)
LenguajePythonGo
RendimientoInicio más lentoSignificativamente más rápido
InstalaciónInstalación separada requeridaIncluido con Docker Engine
EstadoObsoleto (fin de vida junio 2023)En desarrollo activo

Si instalaste Docker siguiendo nuestra guía, el plugin Compose V2 ya está disponible como docker compose (nota el espacio en lugar del guion). Todos los ejemplos en este artículo usan la sintaxis de V2.

Importante: Si encuentras scripts o documentación que hacen referencia a docker-compose (con guion), reemplázalo por docker compose (con espacio). La funcionalidad es la misma, pero V1 ya no recibe mantenimiento.

Anatomía de un Archivo docker-compose.yml

Un archivo Compose tiene tres secciones principales: services, networks y volumes. Esta es la estructura básica:

# Opcional: especificar la versión del archivo Compose (no requerido en V2)
services:
  # Define los servicios de tu aplicación aquí
  web:
    image: nginx:latest
    ports:
      - "80:80"

  database:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secretpassword

networks:
  # Define redes personalizadas aquí (opcional)

volumes:
  # Define volúmenes nombrados aquí (opcional)

Servicios

Los servicios son los contenedores que componen tu aplicación. Cada definición de servicio puede incluir:

  • image o build: La imagen Docker a usar o un Dockerfile desde el cual construir
  • ports: Mapeo de puertos entre el host y el contenedor
  • environment: Variables de entorno
  • volumes: Puntos de montaje para persistencia de datos
  • depends_on: Dependencias de inicio de servicios
  • restart: Política de reinicio
  • networks: A qué redes conectarse
  • command: Sobrescribir el comando predeterminado del contenedor

Redes

Por defecto, Docker Compose crea una única red para tu aplicación y todos los servicios pueden comunicarse entre sí usando sus nombres de servicio como nombres de host. Puedes definir redes personalizadas para un control más granular sobre la comunicación entre servicios:

services:
  web:
    networks:
      - frontend
      - backend

  api:
    networks:
      - backend

  database:
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # Sin acceso externo

En este ejemplo, el servicio web puede comunicarse tanto con api como con database a través de la red backend, mientras que api y database están aislados del acceso externo por el flag internal: true en la red backend. La red frontend podría usarse para el tráfico desde un proxy inverso.

Volúmenes

Los volúmenes persisten datos más allá del ciclo de vida de un contenedor. Los volúmenes nombrados son el enfoque recomendado:

services:
  database:
    image: mysql:8.0
    volumes:
      - db_data:/var/lib/mysql       # Volumen nombrado
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # Montaje bind

volumes:
  db_data:
    driver: local

Variables de Entorno y Archivos .env

Codificar secretos y valores de configuración directamente en tu docker-compose.yml es una mala práctica. Docker Compose soporta archivos .env para gestionar variables de entorno de forma limpia.

Crea un archivo .env en el mismo directorio que tu docker-compose.yml:

# .env
MYSQL_ROOT_PASSWORD=supersecretpassword
MYSQL_DATABASE=myapp
MYSQL_USER=appuser
MYSQL_PASSWORD=apppassword
NGINX_PORT=8080

Referencia estas variables en tu archivo Compose:

services:
  database:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}

  web:
    image: nginx:latest
    ports:
      - "${NGINX_PORT}:80"

También puedes pasar una directiva env_file para cargar variables de entorno directamente en un contenedor:

services:
  api:
    image: myapp:latest
    env_file:
      - ./app.env

Consejo de Seguridad: Siempre añade los archivos .env a tu .gitignore para evitar subir secretos accidentalmente al control de versiones. Proporciona un archivo .env.example con valores de ejemplo para fines de documentación.

Ejemplo Práctico 1: Stack Nginx + PHP-FPM + MySQL

Este es un stack clásico de aplicación web que muchos administradores de sistemas necesitan desplegar:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./app:/var/www/html:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      php:
        condition: service_started
    networks:
      - webnet
    restart: unless-stopped

  php:
    image: php:8.3-fpm-alpine
    volumes:
      - ./app:/var/www/html
      - ./php/php.ini:/usr/local/etc/php/php.ini:ro
    environment:
      DB_HOST: mysql
      DB_NAME: ${MYSQL_DATABASE}
      DB_USER: ${MYSQL_USER}
      DB_PASSWORD: ${MYSQL_PASSWORD}
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - webnet
    restart: unless-stopped

  mysql:
    image: mysql:8.0
    volumes:
      - mysql_data:/var/lib/mysql
      - ./mysql/init:/docker-entrypoint-initdb.d:ro
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - webnet
    restart: unless-stopped

networks:
  webnet:
    driver: bridge

volumes:
  mysql_data:

Puntos clave sobre esta configuración:

  • Las comprobaciones de salud en MySQL aseguran que PHP-FPM no inicie hasta que la base de datos esté realmente lista para aceptar conexiones, no solo cuando el contenedor se inicia.
  • Los montajes de solo lectura (:ro) se usan para archivos de configuración y certificados SSL como medida de seguridad.
  • Los volúmenes nombrados (mysql_data) persisten los datos de la base de datos independientemente del ciclo de vida del contenedor.
  • restart: unless-stopped asegura que los servicios se recuperen automáticamente de fallos pero permanezcan detenidos si los detienes manualmente.

Ejemplo Práctico 2: Stack de Monitoreo con Prometheus + Grafana

Un stack de monitoreo es esencial para cualquier entorno de producción:

services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=30d'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
    networks:
      - monitoring
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
      GF_USERS_ALLOW_SIGN_UP: "false"
    depends_on:
      - prometheus
    networks:
      - monitoring
    restart: unless-stopped

  node-exporter:
    image: prom/node-exporter:latest
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    networks:
      - monitoring
    restart: unless-stopped

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    privileged: true
    devices:
      - /dev/kmsg:/dev/kmsg
    networks:
      - monitoring
    restart: unless-stopped

networks:
  monitoring:
    driver: bridge

volumes:
  prometheus_data:
  grafana_data:

Este stack proporciona métricas a nivel de contenedor (cAdvisor), métricas a nivel de host (Node Exporter), almacenamiento y consulta de métricas (Prometheus) y paneles de visualización (Grafana).

Comandos Comunes de Docker Compose

Aquí tienes una referencia completa de los comandos que usarás con más frecuencia:

# Iniciar todos los servicios en segundo plano
docker compose up -d

# Iniciar y forzar la reconstrucción de imágenes
docker compose up -d --build

# Detener todos los servicios
docker compose down

# Detener y eliminar volúmenes (PRECAUCIÓN: destruye datos)
docker compose down -v

# Ver servicios en ejecución
docker compose ps

# Ver logs de todos los servicios
docker compose logs

# Seguir logs de un servicio específico
docker compose logs -f nginx

# Reiniciar un servicio específico
docker compose restart php

# Escalar un servicio (ejecutar múltiples instancias)
docker compose up -d --scale worker=3

# Ejecutar un comando en un contenedor de servicio en ejecución
docker compose exec mysql mysql -u root -p

# Descargar las últimas imágenes de todos los servicios
docker compose pull

# Ver la configuración resuelta de Compose
docker compose config

# Listar todos los proyectos Compose ejecutándose en el sistema
docker compose ls

Consejo: El comando docker compose config es extremadamente útil para depuración. Muestra el YAML completamente resuelto después de la sustitución de variables, mostrándote exactamente lo que Docker Compose ejecutará.

Consideraciones para Producción

Políticas de Reinicio

Siempre establece una política de reinicio para servicios en producción:

services:
  web:
    restart: unless-stopped  # Recomendado para la mayoría de servicios

Políticas disponibles:

  • no (predeterminado): Nunca reiniciar
  • always: Siempre reiniciar, incluso si se detiene manualmente
  • on-failure: Reiniciar solo si el contenedor sale con un código distinto de cero
  • unless-stopped: Reiniciar a menos que sea detenido explícitamente por el administrador

Comprobaciones de Salud

Las comprobaciones de salud permiten a Docker saber si un servicio está realmente funcionando, no solo ejecutándose:

services:
  api:
    image: myapi:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Las comprobaciones de salud son usadas por las condiciones de depends_on y por Docker para determinar cuándo reiniciar contenedores no saludables.

Límites de Recursos

Evita que un solo servicio consuma todos los recursos disponibles del sistema:

services:
  worker:
    image: myworker:latest
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M

Nota: Los límites de recursos bajo deploy.resources funcionan con docker compose up en versiones recientes de Compose V2. En versiones anteriores, solo se aplicaban al modo Docker Swarm.

Configuración de Logging

Configura el logging por servicio para prevenir el agotamiento del disco:

services:
  web:
    image: nginx:latest
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Mejores Prácticas de Seguridad

  • Nunca ejecutes contenedores como root a menos que sea absolutamente necesario. Usa la directiva user.
  • Establece read_only: true en contenedores que no necesitan escribir en su sistema de archivos.
  • Elimina capacidades Linux innecesarias usando cap_drop: [ALL] y agrega de vuelta solo las necesarias con cap_add.
  • Usa gestión de secretos para datos sensibles en lugar de variables de entorno en texto plano en producción.
services:
  api:
    image: myapi:latest
    read_only: true
    user: "1000:1000"
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    tmpfs:
      - /tmp

Solución de Problemas de Docker Compose

Un Servicio No Puede Conectarse a Otro Servicio

Si un servicio no puede alcanzar a otro, verifica que estén en la misma red y usa el nombre del servicio (no el nombre del contenedor) como nombre de host:

# Verificar a qué redes está conectado un servicio
docker compose exec web ping database

# Inspeccionar la red
docker network ls
docker network inspect <nombre_del_proyecto>_default

Conflictos de Puertos

Si ves bind: address already in use, otro proceso está usando ese puerto:

# Encontrar qué está usando el puerto 80
sudo lsof -i :80
# o
sudo ss -tlnp | grep :80

Cambia el mapeo de puerto del host en tu archivo Compose, por ejemplo de "80:80" a "8080:80".

Los Contenedores Siguen Reiniciándose

Revisa los logs para entender por qué un contenedor está fallando:

docker compose logs --tail=50 <nombre_del_servicio>

# Verificar el código de salida del contenedor
docker compose ps -a

Las causas comunes incluyen variables de entorno faltantes, permisos de archivo incorrectos en volúmenes montados y dependencias que no están listas.

Rendimiento Lento de Bind Mounts en macOS

Si estás desarrollando en macOS y experimentas acceso lento a archivos con bind mounts, considera usar volúmenes Docker en lugar de bind mounts para directorios grandes, o usa la opción de montaje :cached:

volumes:
  - ./app:/var/www/html:cached

Conclusión

Docker Compose transforma la manera en que los administradores de sistemas despliegan y gestionan aplicaciones multi-contenedor. Al definir tu infraestructura en un archivo YAML declarativo, obtienes reproducibilidad, control de versiones y un flujo de trabajo de despliegue con un solo comando. Los ejemplos y mejores prácticas cubiertos en esta guía deberían darte una base sólida para desplegar desde stacks web simples hasta soluciones de monitoreo completas.

Para tus próximos pasos, considera explorar:

  • Perfiles de Docker Compose para gestionar diferentes entornos (desarrollo, staging, producción)
  • Docker Compose watch para actualizaciones automáticas de servicios durante el desarrollo
  • Herramientas de orquestación como Docker Swarm o Kubernetes para despliegues multi-host

Asegúrate de que Docker esté instalado correctamente en tu sistema primero. Si aún no lo has hecho, sigue nuestra guía completa de instalación: Cómo Instalar Docker en Ubuntu 22.04 y 24.04.