Despliegue en producción con Docker Compose: 7 estrategias clave desde health checks hasta actualizaciones sin tiempo de inactividad

DevOps

"Funciona en mi máquina" — El cementerio de contenedores en producción

El mantra de todo desarrollador: "Funciona en mi máquina." Pero cuando los contenedores llegan a producción, la verdadera pesadilla comienza:

  • Contenedores silenciosamente OOM Killed, dejando solo Out of memory en los logs
  • Base de datos aún no lista, contenedores de la aplicación gritando Connection refused
  • Contenedor se cae a las 3 AM sin política de reinicio — servicio caído hasta la mañana
  • Archivos de log llenando discos, docker logs mostrando decenas de GB de texto no estructurado
  • Credenciales de producción en texto plano dentro de docker-compose.yml, contraseñas de base de datos expuestas

Si todavía usas docker compose up -d como toda tu estrategia de producción, este artículo es para ti.

Referencia de conceptos clave

Concepto Propósito Configuración clave en producción
Health Check Detectar si un contenedor está realmente listo healthcheck + depends_on.condition
Límites de recursos Limitar CPU/memoria, prevenir consumo excesivo deploy.resources.limits
Política de reinicio Reinicio automático ante salida anormal deploy.restart_policy
Secrets Almacenamiento cifrado para datos sensibles secrets + Docker Secret
Driver de logging Logging estructurado + rotación de logs logging.driver + logging.options
Profiles Inicio selectivo de servicios por entorno profiles
Watch Sincronización automática de cambios de archivos a contenedores watch (Compose Watch)

5 Desafíos de producción

Desafío 1: Orden de inicio de contenedores incontrolable

La base de datos todavía se está inicializando mientras el contenedor de la aplicación intenta conectarse, causando fallos de inicio. depends_on solo garantiza el orden de inicio, no la disponibilidad del servicio.

Desafío 2: Expansión ilimitada de recursos

Contenedores sin límites de recursos son como autos sin frenos. Un solo contenedor con fuga de memoria puede consumir toda la memoria del host y derribar todos los servicios.

Desafío 3: El agujero negro de los logs

El driver de log json-file por defecto no rota. Después de 3 meses de ejecución, /var/lib/docker llena el disco y todos los servicios se caen.

Desafío 4: Exposición de datos sensibles

Contraseñas de base de datos en bloques environment en texto plano, archivos .env subidos a Git, claves API hardcodeadas en imágenes — estas son bombas de tiempo para incidentes de producción.

Desafío 5: Las actualizaciones significan tiempo de inactividad

docker compose up -d detiene los contenedores antiguos antes de iniciar los nuevos por defecto, haciendo que el servicio no esté disponible durante las actualizaciones. Para servicios 24/7, esto es inaceptable.

7 Patrones de producción

Patrón 1: Health checks y ordenamiento de dependencias

Problema: depends_on solo controla el orden de inicio, no garantiza la disponibilidad del servicio.

Solución: Usar healthcheck + depends_on.condition: service_healthy.

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  app:
    image: myapp:latest
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - "3000:3000"

Parámetros clave:

  • interval: Intervalo de verificación, se recomienda 5-10 segundos para producción
  • timeout: Tiempo de espera de una sola verificación, se recomienda 3-5 segundos
  • retries: Fallos consecutivos antes de marcar como unhealthy
  • start_period: Período de gracia después del inicio del contenedor, los fallos no cuentan hacia los reintentos

Patrón 2: Límites de recursos y protección OOM

Problema: Contenedores sin límites compiten por los recursos del host. Un contenedor descontrolado puede derribar toda la máquina.

Solución: Usar deploy.resources para establecer límites y reservas de CPU y memoria.

services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3

  worker:
    image: myworker:latest
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
        reservations:
          memory: 512M

Límites vs Reservas:

  • limits: Techo duro, si se excede → OOM Kill o throttling de CPU
  • reservations: Garantía suave, el planificador intenta cumplirla pero no la impone

Estrategia de protección OOM:

services:
  critical-service:
    image: critical-app:latest
    deploy:
      resources:
        limits:
          memory: 1G
    cap_add:
      - SYS_PTRACE

A nivel del host, configura vm.overcommit_memory y ajusta las políticas OOM:

# Verificar OOM Score del contenedor
docker inspect --format='{{.State.OOMKilled}}' <container_id>

# Establecer política OOM del host: no matar procesos críticos
echo -1000 > /proc/<pid>/oom_score_adj

Patrón 3: Logging estructurado y rotación de logs

Problema: El driver de log json-file por defecto no rota; los discos se llenan con el tiempo.

Solución: Configurar driver de log + política de rotación. Se recomienda el driver local para producción.

services:
  app:
    image: myapp:latest
    logging:
      driver: local
      options:
        max-size: "10m"
        max-file: "5"
        tag: "{{.Name}}/{{.ID}}"

  nginx:
    image: nginx:1.27-alpine
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "10"
        tag: "nginx/{{.Name}}"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

Comparación de drivers de log:

Driver Caso de uso Ventajas Desventajas
local Predeterminado en producción Auto-rotación, almacenamiento comprimido Solo local
json-file Se necesitan logs JSON estructurados Soporte nativo de Docker Requiere configuración manual de rotación
syslog Recolección centralizada de logs Puede enviar a remoto Configuración compleja
fluentd Integración con stack EFK Enrutamiento flexible de logs Requiere despliegue de Fluentd

Logging estructurado a nivel de aplicación:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

ENV TZ=UTC
ENV LOG_FORMAT=json

CMD ["node", "server.js"]
const logger = {
  info: (msg, meta = {}) => {
    console.log(JSON.stringify({ level: 'info', msg, ts: new Date().toISOString(), ...meta }));
  },
  error: (msg, meta = {}) => {
    console.error(JSON.stringify({ level: 'error', msg, ts: new Date().toISOString(), ...meta }));
  }
};

Patrón 4: Gestión de Secrets

Problema: Datos sensibles almacenados en texto plano en archivos compose o variables de entorno.

Solución: Docker Secrets + variables de entorno con sufijo _FILE.

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt
  jwt_secret:
    file: ./secrets/jwt_secret.txt

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

  app:
    image: myapp:latest
    environment:
      DATABASE_URL: postgresql://appuser:${DB_PASSWORD}@postgres:5432/appdb
      API_KEY_FILE: /run/secrets/api_key
      JWT_SECRET_FILE: /run/secrets/jwt_secret
    secrets:
      - api_key
      - jwt_secret

Gestión de archivos de secrets:

# Crear directorio de secrets
mkdir -p secrets
chmod 700 secrets

# Escribir archivos de secrets
echo "my-super-secret-password-2026" > secrets/db_password.txt
echo "ak-live-xxxx-yyyy-zzzz" > secrets/api_key.txt
echo "jwt-hs256-secret-key-here" > secrets/jwt_secret.txt

# Establecer permisos: solo legible por root
chmod 600 secrets/*.txt

.gitignore debe incluir:

secrets/
*.secret
.env.production
.env.staging

Comparación Docker Secrets vs .env:

Característica Docker Secrets Archivos .env
Almacenamiento cifrado Sí (en modo Swarm) No
Permisos de archivo Restringidos (/run/secrets/) Depende del filesystem
Pista de auditoría No
Sincronización entre nodos Swarm sincroniza automáticamente Distribución manual
Caso de uso Swarm/producción de un solo host Desarrollo

Patrón 5: Actualizaciones rolling sin tiempo de inactividad

Problema: docker compose up -d detiene los contenedores antiguos antes de iniciar los nuevos por defecto.

Solución: Usar docker compose up --no-down + health checks + proxy inverso.

services:
  app:
    image: myapp:${APP_VERSION:-latest}
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
        failure_action: rollback
      rollback_config:
        parallelism: 0
        order: stop-first
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 5s
      timeout: 3s
      retries: 3
      start_period: 15s
    labels:
      - "com.toolsku.app=true"
    ports:
      - "3000-3001:3000"

  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      app:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/health"]
      interval: 10s
      timeout: 3s
      retries: 3

Configuración de proxy inverso Nginx:

upstream app_backend {
    server app:3000;
}

server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://app_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;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_next_upstream error timeout http_502 http_503;
    }

    location /health {
        access_log off;
        return 200 'ok';
        add_header Content-Type text/plain;
    }
}

Script de actualización sin tiempo de inactividad:

#!/bin/bash
set -euo pipefail

NEW_IMAGE="myapp:v2.0.0"
echo "🚀 Iniciando actualización sin tiempo de inactividad a ${NEW_IMAGE}"

# 1. Descargar nueva imagen
docker compose pull app

# 2. Iniciar nuevos contenedores (sin detener los antiguos)
docker compose up -d --no-deps --scale app=2 app

# 3. Esperar a que los nuevos contenedores estén saludables
echo "⏳ Esperando a que los nuevos contenedores estén saludables..."
sleep 15

# 4. Verificar salud del nuevo contenedor
for i in $(seq 1 30); do
  if curl -sf http://localhost:3000/health > /dev/null 2>&1; then
    echo "✅ El nuevo contenedor está saludable"
    break
  fi
  if [ $i -eq 30 ]; then
    echo "❌ Health check fallido, revirtiendo..."
    docker compose up -d --no-deps --scale app=1 app
    exit 1
  fi
  sleep 2
done

# 5. Reducir a 1 réplica
docker compose up -d --no-deps --scale app=1 app

echo "🎉 Actualización completada exitosamente"

Patrón 6: Stack de monitoreo Prometheus + Grafana

Problema: Ejecutar producción sin monitoreo es volar a ciegas — solo te enteras de los problemas por las quejas de los usuarios.

Solución: Desplegar un stack completo de monitoreo Prometheus + Grafana.

services:
  prometheus:
    image: prom/prometheus:v2.52.0
    container_name: prometheus
    restart: unless-stopped
    ports:
      - "9090:9090"
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
      - '--storage.tsdb.retention.size=5GB'
    deploy:
      resources:
        limits:
          memory: 512M
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:11.0.0
    container_name: grafana
    restart: unless-stopped
    ports:
      - "3001:3000"
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD_FILE: /run/secrets/grafana_password
    secrets:
      - grafana_password
    volumes:
      - grafana_data:/var/lib/grafana
    deploy:
      resources:
        limits:
          memory: 256M
    depends_on:
      - prometheus
    networks:
      - monitoring

  node-exporter:
    image: prom/node-exporter:v1.8.0
    container_name: node-exporter
    restart: unless-stopped
    ports:
      - "9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--path.rootfs=/rootfs'
    networks:
      - monitoring

  alertmanager:
    image: prom/alertmanager:v0.27.0
    container_name: alertmanager
    restart: unless-stopped
    ports:
      - "9093:9093"
    volumes:
      - ./monitoring/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
    networks:
      - monitoring

secrets:
  grafana_password:
    file: ./secrets/grafana_password.txt

volumes:
  prometheus_data:
  grafana_data:

networks:
  monitoring:
    driver: bridge

Configuración de Prometheus:

# monitoring/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - "alert_rules.yml"

alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - alertmanager:9093

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

  - job_name: 'app'
    static_configs:
      - targets: ['app:3000']
    metrics_path: /metrics

Reglas de alertas:

# monitoring/alert_rules.yml
groups:
  - name: container_alerts
    rules:
      - alert: ContainerDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Container {{ $labels.instance }} is down"

      - alert: HighMemoryUsage
        expr: process_resident_memory_bytes / (1024 * 1024) > 400
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Memory usage exceeds 400MB on {{ $labels.instance }}"

      - alert: DiskSpaceLow
        expr: node_filesystem_avail_bytes / node_filesystem_size_bytes < 0.1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Disk space below 10% on {{ $labels.instance }}"

Patrón 7: Configuración multi-entorno (dev/staging/prod)

Problema: Las configuraciones de dev, staging y producción están mezcladas — cambiar la configuración de un entorno corre el riesgo de afectar a otros.

Solución: Usar docker-compose.override.yml + estrategia de superposición multi-archivo.

Estructura de directorios:

project/
├── docker-compose.yml              # Configuración base
├── docker-compose.override.yml     # Sobrescritura de dev (cargada automáticamente)
├── docker-compose.staging.yml      # Sobrescritura de staging
├── docker-compose.prod.yml         # Sobrescritura de producción
├── .env                            # Variables de entorno predeterminadas
├── .env.staging                    # Variables de entorno de staging
├── .env.prod                       # Variables de entorno de producción
├── monitoring/
│   ├── prometheus.yml
│   └── alertmanager.yml
└── secrets/
    ├── db_password.txt
    ├── api_key.txt
    └── grafana_password.txt

Configuración base docker-compose.yml:

services:
  app:
    image: myapp:${APP_VERSION:-latest}
    environment:
      NODE_ENV: ${NODE_ENV:-development}
      DATABASE_URL: postgresql://appuser:${DB_PASSWORD}@postgres:5432/appdb
      REDIS_URL: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-network

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - app-network

secrets:
  db_password:
    file: ./secrets/db_password.txt

volumes:
  postgres_data:

networks:
  app-network:
    driver: bridge

Sobrescritura de dev docker-compose.override.yml (cargada automáticamente):

services:
  app:
    build: .
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - "3000:3000"
      - "9229:9229"
    environment:
      NODE_ENV: development
      LOG_LEVEL: debug
    deploy:
      resources:
        limits:
          memory: 512M
    command: node --inspect=0.0.0.0:9229 server.js

  adminer:
    image: adminer:latest
    ports:
      - "8080:8080"
    networks:
      - app-network

Sobrescritura de producción docker-compose.prod.yml:

services:
  app:
    image: myapp:${APP_VERSION}
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      LOG_LEVEL: info
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: '2.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    logging:
      driver: local
      options:
        max-size: "10m"
        max-file: "5"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 15s

  postgres:
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
        reservations:
          memory: 1G
    logging:
      driver: local
      options:
        max-size: "50m"
        max-file: "10"

  redis:
    deploy:
      resources:
        limits:
          memory: 512M
    command: redis-server --appendonly yes --maxmemory 400mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    logging:
      driver: local
      options:
        max-size: "10m"
        max-file: "3"

  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.prod.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      app:
        condition: service_healthy
    deploy:
      resources:
        limits:
          memory: 128M
    logging:
      driver: local
      options:
        max-size: "50m"
        max-file: "10"

volumes:
  redis_data:

Comandos de inicio:

# Desarrollo (carga override automáticamente)
docker compose up -d

# Staging
docker compose -f docker-compose.yml -f docker-compose.staging.yml --env-file .env.staging up -d

# Producción
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.prod up -d

5 Errores comunes

Error 1: depends_on no significa que el servicio esté listo

Incorrecto:

services:
  app:
    depends_on:
      - postgres
    # El contenedor postgres se inició, pero la BD puede no estar inicializada aún

Correcto:

services:
  app:
    depends_on:
      postgres:
        condition: service_healthy
  postgres:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

Error 2: Sin límites de recursos

Incorrecto:

services:
  app:
    image: myapp:latest
    # Sin límites de recursos — una fuga de memoria puede consumir todo el host

Correcto:

services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M

Error 3: Sin rotación de logs

Incorrecto:

services:
  app:
    image: myapp:latest
    # Driver json-file por defecto, los logs crecen indefinidamente

Correcto:

services:
  app:
    image: myapp:latest
    logging:
      driver: local
      options:
        max-size: "10m"
        max-file: "5"

Error 4: Datos sensibles en texto plano

Incorrecto:

services:
  postgres:
    environment:
      POSTGRES_PASSWORD: "my-secret-password-123"
      # Contraseña en texto plano en el archivo compose — desastre si se sube a Git

Correcto:

services:
  postgres:
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Error 5: Usar el tag latest

Incorrecto:

services:
  app:
    image: myapp:latest
    # Cada pull puede obtener una imagen diferente — no es reproducible

Correcto:

services:
  app:
    image: myapp:2.1.0
    # O usar una variable para control de versiones
    image: myapp:${APP_VERSION:-2.1.0}

Referencia de solución de errores

Mensaje de error Causa Solución
OOMKilled El contenedor excedió el límite de memoria Aumentar límite memory u optimizar uso de memoria de la app
Connection refused Servicio dependiente no listo Agregar healthcheck + depends_on.condition
no space left on device Logs/imagenes llenando el disco Configurar rotación logging + docker system prune
Bucle de restarting La app falla al iniciar Verificar docker logs <id> y confirmar configuración
permission denied Problema de permisos de archivo/directorio Verificar directiva user y permisos de volúmenes
port is already allocated Conflicto de puertos Cambiar mapeo de puertos o detener proceso en conflicto
Estado unhealthy Health check fallando Verificar que el comando healthcheck sea correcto
secret not found Archivo de secret faltante Asegurar que el archivo correspondiente exista en secrets/
Cannot connect to the Docker daemon Docker no está ejecutándose systemctl start docker
image pulling failed Fallo al descargar imagen Verificar red/autenticación de registry/ortografía del nombre de imagen

Optimización avanzada

Docker Compose Watch para desarrollo

Compose Watch sincroniza automáticamente los cambios de archivos a los contenedores sin reconstruir imágenes:

services:
  app:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: ./package.json
        - action: sync+restart
          path: ./config
          target: /app/config
# Iniciar modo watch
docker compose watch

Aislamiento de red y seguridad

services:
  app:
    networks:
      - frontend
      - backend

  postgres:
    networks:
      - backend
    # postgres no está en la red frontend — acceso externo bloqueado

  nginx:
    networks:
      - frontend
    ports:
      - "80:80"

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true
    # internal:true deshabilita el acceso externo

Optimización de imágenes con multi-stage builds

# ---- Etapa de build ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# ---- Etapa de runtime ----
FROM node:20-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

USER appuser
EXPOSE 3000

HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

CMD ["node", "dist/server.js"]

Comparación de herramientas de orquestación

Característica Docker Compose Kubernetes Nomad Docker Swarm
Complejidad Baja Alta Media Baja
Despliegue de un solo host ✅ Excelente ❌ Excesivo ⚠️ Utilizable ✅ Excelente
Orquestación multi-host ❌ No soportado ✅ Capacidad central ✅ Capacidad central ⚠️ Básico
Auto-scaling ✅ HPA/VPA ⚠️ Manual
Actualizaciones rolling ⚠️ Requiere scripts ✅ Nativo ✅ Nativo ✅ Nativo
Service discovery ⚠️ DNS ✅ CoreDNS ✅ Consul ✅ DNS
Orquestación de almacenamiento ✅ CSI ✅ CSI ⚠️ Básico
Curva de aprendizaje Baja Alta Media Baja
Escala adecuada 1-10 servicios 100+ servicios 50+ servicios 10-50 servicios
Listo para producción ✅ Un solo host ✅ Gran escala ✅ Mediana a gran escala ⚠️ Comunidad en declive

Recomendación: Usa Docker Compose para producción de un solo host/pequeña escala, Kubernetes para gran escala, Nomad si estás en el ecosistema HashiCorp. Docker Swarm está cada vez más marginado — no recomendado para nuevos proyectos.

Resumen

El despliegue en producción con Docker Compose no es solo docker compose up -d. Los health checks aseguran que los servicios estén realmente listos, los límites de recursos previenen avalanchas OOM, la rotación de logs evita el agotamiento del disco, los Secrets protegen datos sensibles, las actualizaciones sin tiempo de inactividad garantizan disponibilidad 24/7, los stacks de monitoreo eliminan puntos ciegos, y las configuraciones multi-entorno mantienen dev/staging/prod correctamente separados. Domina estas 7 estrategias, y Docker Compose es totalmente capaz de despliegues de producción a pequeña y mediana escala.

Herramientas recomendadas

  • Formateador JSON - Formatear configuraciones JSON relacionadas con Docker Compose YAML
  • Codificación Base64 - Codificar Secrets y datos de configuración sensibles
  • Calculadora de Hash - Generar checksums para archivos de configuración y asegurar consistencia en los despliegues

Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →

#Docker#Docker Compose#生产部署#容器编排#2026#DevOps