Docker Compose: Guía Completa con Ejemplos Prácticos

Docker Compose simplifica la orquestación de aplicaciones multi-contenedor al definir y ejecutar aplicaciones complejas usando un único archivo de configuración YAML. Esta guía completa cubre todo desde conceptos básicos hasta despliegues de producción avanzados con ejemplos del mundo real.

Introducción a Docker Compose

Docker Compose es una herramienta para definir y ejecutar aplicaciones Docker multi-contenedor. En lugar de gestionar contenedores individualmente con extensos comandos docker run, Compose usa un archivo YAML para configurar todos los servicios, redes y volúmenes de la aplicación en un solo lugar.

¿Por Qué Usar Docker Compose?

  • Gestión Simplificada: Define el stack completo de la aplicación en un archivo
  • Reproducibilidad: Comparte configuraciones con tu equipo
  • Eficiencia de Desarrollo: Inicia el stack completo con un solo comando
  • Consistencia de Entorno: Misma configuración en dev, test y staging
  • Dependencias de Servicios: Define orden de inicio y dependencias
  • Escalado Fácil: Escala servicios hacia arriba o abajo con un comando

Docker Compose vs Kubernetes

  • Docker Compose: Desarrollo de host único y despliegues pequeños
  • Kubernetes: Orquestación de producción multi-host a escala

Requisitos Previos

Antes de comenzar, asegúrate de tener:

  • Docker Engine instalado (versión 20.10 o superior)
  • Docker Compose instalado (versión 2.0 o superior)
  • Conocimiento básico de Docker (imágenes, contenedores, volúmenes)
  • Editor de texto para archivos YAML
  • Comprensión de la arquitectura de tu aplicación

Verifica la instalación:

# Check Docker Compose version
docker compose version

# Or older standalone version
docker-compose --version

Notas de Instalación

Docker Compose v2 ahora está integrado como plugin de Docker CLI:

# New syntax (recommended)
docker compose up

# Old syntax (standalone)
docker-compose up

Esta guía usa la nueva sintaxis docker compose.

Conceptos Básicos de Docker Compose

Creando Tu Primer Archivo Compose

Crea un archivo llamado docker-compose.yml:

version: '3.8'

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"

Inicia la aplicación:

docker compose up

Detén la aplicación:

docker compose down

Flujo de Trabajo Básico

# Start services
docker compose up -d

# View running services
docker compose ps

# View logs
docker compose logs

# Stop services
docker compose down

Estructura del Archivo Compose

Especificación de Versión

# Compose file format version
version: '3.8'

Nota: La versión 3.8 es ampliamente soportada. Versiones más nuevas (3.9, 3.10) agregan características.

Claves de Nivel Superior

version: '3.8'

services:      # Define containers
  service1:
  service2:

networks:      # Define networks
  network1:

volumes:       # Define volumes
  volume1:

configs:       # Define configs (Swarm mode)
  config1:

secrets:       # Define secrets (Swarm mode)
  secret1:

Ejemplo Mínimo

version: '3.8'

services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"

Configuración de Servicios

Servicios Basados en Imagen

services:
  database:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"

Servicios Basados en Construcción

services:
  app:
    build: .                    # Build from current directory
    ports:
      - "3000:3000"

  api:
    build:
      context: ./api           # Build context
      dockerfile: Dockerfile.prod  # Custom Dockerfile
      args:                    # Build arguments
        NODE_ENV: production

Nomenclatura de Contenedores

services:
  web:
    container_name: my-web-server
    image: nginx:alpine

Mapeo de Puertos

services:
  web:
    image: nginx
    ports:
      - "8080:80"              # host:container
      - "8443:443"
      - "127.0.0.1:9000:9000"  # bind to specific interface
      - "3000-3005:3000-3005"  # port range

Variables de Entorno

services:
  app:
    image: node:18-alpine
    environment:
      NODE_ENV: production
      API_KEY: ${API_KEY}      # From host environment
      DATABASE_URL: postgres://db:5432/mydb

Archivos de Entorno

services:
  app:
    image: node:18-alpine
    env_file:
      - .env
      - .env.production

Archivo .env:

NODE_ENV=production
API_KEY=your_api_key_here
DATABASE_URL=postgres://db:5432/mydb

Volúmenes

services:
  app:
    image: node:18-alpine
    volumes:
      - ./app:/usr/src/app           # bind mount
      - app-data:/app/data            # named volume
      - /var/run/docker.sock:/var/run/docker.sock:ro  # read-only

Depends On

services:
  web:
    image: nginx
    depends_on:
      - api
      - cache

  api:
    image: node:18-alpine
    depends_on:
      - database

  database:
    image: postgres:15-alpine

  cache:
    image: redis:alpine

Políticas de Reinicio

services:
  web:
    image: nginx
    restart: always            # always, no, on-failure, unless-stopped

Verificaciones de Salud

services:
  web:
    image: nginx
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Límites de Recursos

services:
  app:
    image: myapp
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M

Command y Entrypoint

services:
  app:
    image: node:18-alpine
    command: npm start
    # Or with array syntax
    command: ["npm", "run", "dev"]

  worker:
    image: node:18-alpine
    entrypoint: /docker-entrypoint.sh

Directorio de Trabajo

services:
  app:
    image: node:18-alpine
    working_dir: /usr/src/app

Usuario

services:
  app:
    image: node:18-alpine
    user: "1000:1000"

Redes en Compose

Red Predeterminada

Docker Compose crea automáticamente una red predeterminada:

version: '3.8'

services:
  web:
    image: nginx

  api:
    image: node:18-alpine

Los servicios pueden comunicarse usando nombres de servicios como nombres de host.

Redes Personalizadas

version: '3.8'

services:
  web:
    image: nginx
    networks:
      - frontend

  api:
    image: node:18-alpine
    networks:
      - frontend
      - backend

  database:
    image: postgres
    networks:
      - backend

networks:
  frontend:
  backend:

Configuración de Red

networks:
  frontend:
    driver: bridge

  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16
          gateway: 172.28.0.1

Redes Externas

networks:
  existing-network:
    external: true
    name: my-pre-existing-network

Volúmenes en Compose

Volúmenes Nombrados

version: '3.8'

services:
  database:
    image: postgres:15-alpine
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Configuración de Volumen

volumes:
  db-data:
    driver: local
    driver_opts:
      type: none
      device: /path/on/host
      o: bind

Volúmenes Externos

volumes:
  existing-volume:
    external: true
    name: my-pre-existing-volume

Variables de Entorno

Sustitución de Variables

services:
  web:
    image: nginx:${NGINX_VERSION:-latest}
    ports:
      - "${WEB_PORT:-8080}:80"

Archivo .env

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

NGINX_VERSION=alpine
WEB_PORT=8080
POSTGRES_PASSWORD=secretpassword

Múltiples Archivos de Entorno

services:
  app:
    image: myapp
    env_file:
      - ./common.env
      - ./prod.env

Comandos de Docker Compose

Iniciar Servicios

# Start all services
docker compose up

# Start in detached mode
docker compose up -d

# Start specific services
docker compose up web database

# Force recreate containers
docker compose up --force-recreate

# Build images before starting
docker compose up --build

Detener Servicios

# Stop all services
docker compose stop

# Stop specific service
docker compose stop web

# Stop and remove containers, networks
docker compose down

# Remove volumes too
docker compose down -v

# Remove images too
docker compose down --rmi all

Ver Estado

# List running services
docker compose ps

# Show all services (including stopped)
docker compose ps -a

# View service logs
docker compose logs

# Follow logs
docker compose logs -f

# Logs for specific service
docker compose logs -f web

# Show last 100 lines
docker compose logs --tail=100

Ejecutar Comandos

# Execute command in service
docker compose exec web sh

# Run one-off command
docker compose run web npm install

# Run without dependencies
docker compose run --no-deps web npm test

Construir Imágenes

# Build all services
docker compose build

# Build specific service
docker compose build web

# Build without cache
docker compose build --no-cache

# Build with parallel execution
docker compose build --parallel

Escalar Servicios

# Scale specific service
docker compose up -d --scale web=3

# Scale multiple services
docker compose up -d --scale web=3 --scale worker=5

Validación

# Validate compose file
docker compose config

# Render compose file with variables
docker compose config --resolve-image-digests

Otros Comandos Útiles

# Pause services
docker compose pause

# Unpause services
docker compose unpause

# Restart services
docker compose restart

# View resource usage
docker compose top

# Pull latest images
docker compose pull

Ejemplos del Mundo Real

Ejemplo 1: Stack WordPress

version: '3.8'

services:
  wordpress:
    image: wordpress:latest
    container_name: wordpress
    restart: unless-stopped
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: database
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wordpress-data:/var/www/html
    depends_on:
      - database
    networks:
      - wp-network

  database:
    image: mysql:8.0
    container_name: wordpress-db
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - wp-network

  phpmyadmin:
    image: phpmyadmin:latest
    container_name: phpmyadmin
    restart: unless-stopped
    ports:
      - "8081:80"
    environment:
      PMA_HOST: database
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    depends_on:
      - database
    networks:
      - wp-network

volumes:
  wordpress-data:
  db-data:

networks:
  wp-network:
    driver: bridge

Ejemplo 2: Aplicación Full Stack Node.js

version: '3.8'

services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: react-frontend
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - REACT_APP_API_URL=http://localhost:5000
    volumes:
      - ./frontend:/app
      - /app/node_modules
    networks:
      - app-network

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: node-backend
    restart: unless-stopped
    ports:
      - "5000:5000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@database:5432/myapp
      - REDIS_URL=redis://redis:6379
    volumes:
      - ./backend:/app
      - /app/node_modules
    depends_on:
      - database
      - redis
    networks:
      - app-network

  database:
    image: postgres:15-alpine
    container_name: postgres-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: myapp
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    container_name: redis-cache
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    container_name: nginx-proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - frontend
      - backend
    networks:
      - app-network

volumes:
  postgres-data:
  redis-data:

networks:
  app-network:
    driver: bridge

Ejemplo 3: Microservicios con Cola de Mensajes

version: '3.8'

services:
  api-gateway:
    build: ./api-gateway
    ports:
      - "8080:8080"
    environment:
      - USER_SERVICE_URL=http://user-service:3001
      - ORDER_SERVICE_URL=http://order-service:3002
    depends_on:
      - user-service
      - order-service
    networks:
      - microservices

  user-service:
    build: ./user-service
    environment:
      - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@postgres:5432/users
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - postgres
      - rabbitmq
    networks:
      - microservices

  order-service:
    build: ./order-service
    environment:
      - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@postgres:5432/orders
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - postgres
      - rabbitmq
    networks:
      - microservices

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init-db.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - microservices

  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq
    networks:
      - microservices

volumes:
  postgres-data:
  rabbitmq-data:

networks:
  microservices:
    driver: bridge

Ejemplo 4: Aplicación Python Django

version: '3.8'

services:
  web:
    build: .
    command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - .:/code
      - static-volume:/code/staticfiles
      - media-volume:/code/media
    expose:
      - 8000
    environment:
      - DEBUG=0
      - SECRET_KEY=${SECRET_KEY}
      - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/myapp
      - REDIS_URL=redis://redis:6379/1
    depends_on:
      - db
      - redis
    networks:
      - django-network

  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - static-volume:/code/staticfiles:ro
      - media-volume:/code/media:ro
    ports:
      - "80:80"
    depends_on:
      - web
    networks:
      - django-network

  db:
    image: postgres:15-alpine
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=myapp
    networks:
      - django-network

  redis:
    image: redis:7-alpine
    networks:
      - django-network

  celery:
    build: .
    command: celery -A myproject worker -l info
    volumes:
      - .:/code
    environment:
      - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/myapp
      - REDIS_URL=redis://redis:6379/1
    depends_on:
      - db
      - redis
    networks:
      - django-network

  celery-beat:
    build: .
    command: celery -A myproject beat -l info
    volumes:
      - .:/code
    environment:
      - DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/myapp
      - REDIS_URL=redis://redis:6379/1
    depends_on:
      - db
      - redis
    networks:
      - django-network

volumes:
  postgres-data:
  static-volume:
  media-volume:

networks:
  django-network:
    driver: bridge

Ejemplo 5: Stack de Monitoreo (Prometheus + Grafana)

version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
    ports:
      - "9090:9090"
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
      - GF_INSTALL_PLUGINS=grafana-piechart-panel
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    ports:
      - "3000:3000"
    depends_on:
      - prometheus
    networks:
      - monitoring

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

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    restart: unless-stopped
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    ports:
      - "8080:8080"
    networks:
      - monitoring

volumes:
  prometheus-data:
  grafana-data:

networks:
  monitoring:
    driver: bridge

Mejores Prácticas de Producción

Usar Control de Versiones

Siempre confirma docker-compose.yml en git:

git add docker-compose.yml .env.example
git commit -m "Add Docker Compose configuration"

Archivos Específicos de Entorno

# docker-compose.yml (base)
version: '3.8'
services:
  web:
    image: myapp

# docker-compose.override.yml (development)
version: '3.8'
services:
  web:
    volumes:
      - .:/app
    command: npm run dev

# docker-compose.prod.yml (production)
version: '3.8'
services:
  web:
    restart: unless-stopped
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Ejecuta con configuración específica:

# Development (uses override automatically)
docker compose up

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Mejores Prácticas de Seguridad

services:
  app:
    image: myapp
    # Run as non-root user
    user: "1000:1000"

    # Read-only root filesystem
    read_only: true
    tmpfs:
      - /tmp

    # Drop capabilities
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

    # Security options
    security_opt:
      - no-new-privileges:true

Límites de Recursos

services:
  app:
    image: myapp
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '1'
          memory: 1G

Verificaciones de Salud

services:
  app:
    image: myapp
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 40s

Configuración de Logging

services:
  app:
    image: myapp
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
        labels: "production"

Usar Secrets (Docker Swarm)

version: '3.8'

services:
  app:
    image: myapp
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    external: true

Solución de Problemas

Ver Logs de Servicio

# All services
docker compose logs

# Specific service
docker compose logs web

# Follow logs
docker compose logs -f

# Last 100 lines
docker compose logs --tail=100

El Contenedor No Inicia

# Check status
docker compose ps

# View logs
docker compose logs service-name

# Validate compose file
docker compose config

# Force recreate
docker compose up --force-recreate

Problemas de Red

# Inspect network
docker network ls
docker network inspect project_default

# Recreate network
docker compose down
docker compose up

Problemas de Permisos de Volumen

# Check volume
docker volume inspect project_volume-name

# Fix permissions in container
docker compose exec service-name chown -R user:group /path

Limpiar Recursos

# Stop and remove everything
docker compose down

# Remove volumes too
docker compose down -v

# Remove images
docker compose down --rmi all

# Complete cleanup
docker compose down -v --rmi all --remove-orphans

Depurar Servicio

# Execute shell in running container
docker compose exec service-name sh

# Run one-off command
docker compose run --rm service-name sh

# View container processes
docker compose top service-name

Conclusión

Docker Compose optimiza la gestión de aplicaciones multi-contenedor, haciéndolo esencial para flujos de trabajo de desarrollo modernos. Esta guía cubrió todo desde conceptos básicos hasta configuraciones listas para producción.

Conclusiones Clave

  • Configuración Única: Define el stack completo en docker-compose.yml
  • Dependencias de Servicios: Usa depends_on para orden de inicio
  • Aislamiento de Red: Creación automática de red con descubrimiento de servicios
  • Gestión de Volúmenes: Persiste datos con volúmenes nombrados y bind
  • Flexibilidad de Entorno: Usa archivos .env y sustitución de variables
  • Listo para Producción: Implementa verificaciones de salud, logging y límites de recursos

Referencia Rápida

# Essential Commands
docker compose up -d              # Start services
docker compose down               # Stop services
docker compose ps                 # List services
docker compose logs -f            # View logs
docker compose exec web sh        # Access shell
docker compose build              # Build images
docker compose pull               # Pull images
docker compose restart            # Restart services

# Management
docker compose config             # Validate file
docker compose up --build         # Rebuild and start
docker compose down -v            # Remove with volumes
docker compose scale web=3        # Scale service

Próximos Pasos

  1. Practica: Crea archivos compose para tus proyectos
  2. Optimiza: Implementa construcciones multi-etapa en Dockerfiles
  3. Asegura: Agrega verificaciones de salud y opciones de seguridad
  4. Monitorea: Integra soluciones de logging y monitoreo
  5. Orquesta: Avanza a Kubernetes para producción
  6. Automatiza: Integra con pipelines CI/CD
  7. Documenta: Mantén README completo con instrucciones de configuración

Docker Compose es perfecto para entornos de desarrollo y despliegues de host único. Para orquestación de producción multi-host, considera Kubernetes o Docker Swarm.