🐳 DevOps

Docker en Producción: Guía Completa para Desarrolladores

← Volver al Blog

Introducción

Docker ha revolucionado la forma en que desplegamos aplicaciones. Pero pasar de desarrollo a producción requiere conocimientos adicionales sobre redes, volúmenes, seguridad y orquestación.

Conceptos Fundamentals

Dockerfile Optimizado

# Usa imagen oficial base
FROM node:20-alpine

# Usa usuario no root para seguridad
RUN addgroup -g 1001 -S nodejs && adduser -S nodeapp -u 1001

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY --chown=nodeapp:nodejs . .

USER nodeapp
EXPOSE 3000
CMD ["node", "index.js"]

Docker Compose para Producción

version: '3.8'
services:
  app:
    image: mi-app:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - app-data:/app/data
    networks:
      - backend
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis-data:/data
    networks:
      - backend

volumes:
  app-data:
  redis-data:

networks:
  backend:
    driver: bridge

1. Seguridad en Contenedores

La seguridad es la asignatura pendiente de muchos despliegues Docker en producción. Un contenedor no es una máquina virtual: comparte el kernel del host, lo que amplía la superficie de ataque si no se toman las precauciones adecuadas.

Docker Bench Security

Docker Bench Security es un script oficial que audita tu configuración contra el CIS Docker Benchmark. Evalúa cientos de controles: desde permisos del socket Docker hasta configuración del daemon y hardening del contenedor.

# Ejecutar Docker Bench Security (desde el host)
docker run --rm --net host \
  -v /etc:/etc:ro \
  -v /usr/bin/containerd:/usr/bin/containerd:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  aquasec/docker-bench-security

Revisa las secciones marcadas como WARN y aplícalas una por una. Un CI/CD pipeline debería fallar si el score baja de cierto umbral.

Escaneo de Vulnerabilidades con Trivy y Grype

Escanea todas tus imágenes en busca de CVEs conocidos antes de desplegar. Integra el escaneo en tu pipeline de CI/CD.

# Trivy — escaneo rápido y completo
trivy image mi-app:latest

# Escaneo con salida JSON para integración
trivy image --format json --output report.json mi-app:latest

# Grype — alternativa con Syft para SBOM
grype mi-app:latest

# Generar SBOM con Syft
syft mi-app:latest -o spdx-json > bom.spdx.json

Configura un gate de calidad: si se detectan vulnerabilidades CRITICAL o HIGH, el pipeline debe fallar antes de publicar la imagen al registry.

Non-Root y Read-Only Root Filesystem

Nunca ejecutes contenedores como root. Si un atacante escala dentro del contenedor, tener acceso root dentro del mismo (aunque esté limitado por namespaces) facilita escapes. Combínalo con un sistema de archivos raíz de solo lectura:

# Dockerfile seguro
FROM python:3.12-slim

RUN addgroup --system --gid 1001 appgroup \
    && adduser --system --uid 1001 --gid 1001 appuser

WORKDIR /app
COPY --chown=appuser:appgroup . .

USER appuser

# Read-only + tmpfs para escritura temporal
# docker run --read-only --tmpfs /tmp ...
CMD ["python", "app.py"]
# docker-compose.yml con read-only y tmpfs
services:
  app:
    image: mi-app:latest
    read_only: true
    tmpfs:
      - /tmp:noexec,nosuid,size=64M
      - /run
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

Con read_only: true y tmpfs para directorios temporales, eliminas la posibilidad de que un atacante modifique binarios dentro del contenedor.

2. Monitoreo con Prometheus y Grafana

Sin métricas, estás operando a ciegas. El stack Prometheus + Grafana se ha convertido en el estándar de facto para monitorear contenedores en producción.

cAdvisor: Métricas de Contenedores

cAdvisor (Container Advisor) expone métricas de CPU, memoria, red y disco de cada contenedor en un formato que Prometheus entiende de forma nativa.

# Ejecutar cAdvisor como contenedor
docker run --rm \
  --name cadvisor \
  --volume=/:/rootfs:ro \
  --volume=/var/run:/var/run:ro \
  --volume=/sys:/sys:ro \
  --volume=/var/lib/docker/:/var/lib/docker:ro \
  --publish=8080:8080 \
  --privileged \
  gcr.io/cadvisor/cadvisor:latest

Prometheus: Recopilación y Almacenamiento

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

scrape_configs:
  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']

  - job_name: 'docker-engine'
    static_configs:
      - targets: ['docker.host.internal:9323']
# docker-compose para Prometheus + cAdvisor + Grafana
services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
    ports:
      - "9090:9090"
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - "3001:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=changeme
    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
    ports:
      - "8080:8080"
    privileged: true
    restart: unless-stopped

volumes:
  prometheus-data:
  grafana-data:

Consultas PromQL Esenciales

# Uso de CPU por contenedor
sum(rate(container_cpu_usage_seconds_total{name=~".+"}[5m])) by (name)

# Uso de memoria
sum(container_memory_usage_bytes{name=~".+"}) by (name)

# IO de disco
sum(rate(container_fs_io_current{name=~".+"}[5m])) by (name)

# Red recibida/enviada
sum(rate(container_network_receive_bytes_total{name=~".+"}[5m])) by (name)
sum(rate(container_network_transmit_bytes_total{name=~".+"}[5m])) by (name)

Alertas en Prometheus

# alert.rules.yml
groups:
  - name: container_alerts
    rules:
      - alert: HighMemoryUsage
        expr: (container_memory_usage_bytes{name=~".+"} / container_spec_memory_limit_bytes{name=~".+"}) > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Contenedor {{ $labels.name }} usando más del 85% de memoria"

      - alert: ContainerDown
        expr: time() - container_last_seen{name=~".+"} > 60
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Contenedor {{ $labels.name }} no reporta métricas"

3. Logging Centralizado

Los logs en producción deben estar centralizados. Si dependes de docker logs en cada servidor, estás perdiendo visibilidad. Tienes dos caminos principales: el stack ELK (Elasticsearch, Logstash, Kibana) o la alternativa más ligera Loki + Grafana.

Log Drivers de Docker

Docker soporta múltiples log drivers que determinan cómo se gestionan los logs de los contenedores. El driver por defecto es json-file, pero en producción conviene usar syslog, fluentd, gelf o awslogs.

# Configuración global del daemon en /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
# docker-compose con fluentd como log driver
services:
  app:
    image: mi-app:latest
    logging:
      driver: fluentd
      options:
        fluentd-address: "localhost:24224"
        tag: "{{.Name}}"
        env: "ENVIRONMENT"

Stack ELK para Logs de Contenedores

# docker-compose para ELK reducido
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    volumes:
      - elastic-data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    restart: unless-stopped

  logstash:
    image: docker.elastic.co/logstash/logstash:8.12.0
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro
    ports:
      - "5000:5000"
      - "9600:9600"
    depends_on:
      - elasticsearch
    restart: unless-stopped

  kibana:
    image: docker.elastic.co/kibana/kibana:8.12.0
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch
    restart: unless-stopped

volumes:
  elastic-data:
# logstash.conf — pipeline de entrada para logs Docker
input {
  tcp {
    port => 5000
    codec => json_lines
  }
}

filter {
  if [docker] {
    mutate {
      add_field => { "[@metadata][index]" => "docker-logs-%{+YYYY.MM.dd}" }
    }
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "%{[@metadata][index]}"
  }
}

Loki + Grafana: Alternativa más Ligera

Loki está diseñado específicamente para logs de contenedores. No indexa el contenido del log, solo los metadatos (labels), lo que lo hace mucho más eficiente que Elasticsearch para este caso de uso.

# docker-compose para Loki + Promtail
services:
  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml
    volumes:
      - loki-data:/loki
    restart: unless-stopped

  promtail:
    image: grafana/promtail:latest
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/log:/var/log:ro
      - ./promtail-config.yaml:/etc/promtail/config.yaml:ro
    command: -config.file=/etc/promtail/config.yaml
    restart: unless-stopped

volumes:
  loki-data:
# promtail-config.yaml
scrape_configs:
  - job_name: docker
    static_configs:
      - targets: ['localhost']
        labels:
          job: docker
          __path__: /var/lib/docker/containers/*/*.log
    pipeline_stages:
      - json:
          expressions:
            log: log
            stream: stream
            time: time
            tag: attrs.tag

4. Redes Avanzadas

Docker incluye varios drivers de red. En un host simple, bridge es suficiente, pero en entornos multi-host o cuando necesitas control fino de la red, necesitas opciones más avanzadas.

Overlay Networks (Docker Swarm)

Las redes overlay permiten que contenedores en diferentes hosts se comuniquen de forma transparente. Funcionan encapsulando el tráfico VXLAN entre nodos.

# Crear una red overlay (requiere Docker Swarm)
docker network create \
  --driver overlay \
  --attachable \
  --subnet=10.0.10.0/24 \
  --gateway=10.0.10.1 \
  app-net

# Servicios en diferentes hosts conectados a la misma overlay
docker service create --name app --network app-net --replicas 3 mi-app:latest
docker service create --name redis --network app-net redis:7-alpine

Macvlan: Conecta Contenedores Directamente a la Red Física

Macvlan asigna una dirección MAC única a cada contenedor, haciéndolo aparecer como un dispositivo físico más en la red. Útil para aplicaciones legacy que necesitan su propia IP.

# Crear red Macvlan
docker network create \
  -d macvlan \
  --subnet=192.168.1.0/24 \
  --gateway=192.168.1.1 \
  -o parent=eth0 \
  macvlan-net

# Ejecutar contenedor con IP específica
docker run --rm -d \
  --network macvlan-net \
  --ip=192.168.1.100 \
  nginx:alpine

Advertencia: Macvlan no permite comunicación entre el host y los contenedores en la misma red Macvlan. Para eso necesitas un subinterface o usar ipvlan.

Service Discovery con DNS de Docker

Docker tiene un DNS interno integrado. Cuando usas redes definidas por usuario, los contenedores pueden resolver el nombre de otros servicios automáticamente.

# En docker-compose, los nombres de servicio son resolubles
services:
  api:
    image: mi-api:latest
    networks:
      - internal
    environment:
      - DB_HOST=database  # ← resuelve por DNS interno
      - REDIS_HOST=cache

  database:
    image: postgres:16-alpine
    networks:
      - internal
    environment:
      POSTGRES_DB: myapp

  cache:
    image: redis:7-alpine
    networks:
      - internal

networks:
  internal:
    driver: bridge

Aislamiento y Políticas de Red

Segmenta tus redes para minimizar el impacto de una brecha. Nunca pongas todos los servicios en la misma red.

# Arquitectura de redes segmentadas
networks:
  frontend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/24

  backend:
    driver: bridge
    internal: true  # sin acceso a internet
    ipam:
      config:
        - subnet: 172.20.1.0/24

  monitoring:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.2.0/24

services:
  nginx:
    networks:
      frontend:
        ipv4_address: 172.20.0.10

  app:
    networks:
      frontend:
        ipv4_address: 172.20.0.20
      backend:
        ipv4_address: 172.20.1.10

  database:
    networks:
      backend:
        ipv4_address: 172.20.1.20

  prometheus:
    networks:
      monitoring:
        ipv4_address: 172.20.2.10

5. Gestión de Secretos

Nunca hardcodees contraseñas, tokens o API keys en el Dockerfile o en variables de entorno visibles. Docker ofrece varios mecanismos seguros para gestionar secretos.

Docker Secrets (Docker Swarm)

En modo Swarm, los secretos se almacenan cifrados en la base de datos interna de Swarm y solo se montan en los contenedores que tienen permiso explícito.

# Crear secretos desde archivo
echo "supersecret123" | docker secret create db_password -

# Crear secreto desde stdin
docker secret create api_key -

# Usar secretos en un servicio
docker service create \
  --name app \
  --secret db_password \
  --secret api_key \
  --secret source=ssl_cert,target=/etc/nginx/certs/cert.pem \
  mi-app:latest

Docker monta los secretos como archivos temporales en /run/secrets/ dentro del contenedor. Tu aplicación debe leerlos desde allí:

# Lectura de secretos en Node.js
const fs = require('fs');
const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();
# docker-compose con secrets
services:
  app:
    image: mi-app:latest
    secrets:
      - db_password
      - api_key
    environment:
      - DB_USER=admin
      # DB_PASSWORD se lee desde /run/secrets/db_password

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

Variables de Entorno vs. Secret Mounts

Las variables de entorno son frágiles: cualquier proceso dentro del contenedor puede leer /proc/1/environ. Prefiere siempre montar secretos como archivos.

Si usas variables de entorno para secretos (por ejemplo en Kubernetes Secrets), considera que quedan visibles en docker inspect, en los logs del proceso y en cualquier dump de memoria.

Integración con HashiCorp Vault

Para entornos donde necesitas rotación dinámica de secretos o políticas de acceso granulares, Vault es la solución estándar.

# docker-compose con Vault
services:
  vault:
    image: hashicorp/vault:latest
    cap_add:
      - IPC_LOCK
    environment:
      VAULT_DEV_ROOT_TOKEN_ID: root-token
      VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
    ports:
      - "8200:8200"

  app:
    image: mi-app:latest
    environment:
      VAULT_ADDR: http://vault:8200
      VAULT_TOKEN: root-token
      # La app usa Vault API para obtener secretos dinámicamente
# Ejemplo: obtener secreto de Vault con curl desde app
# curl -H "X-Vault-Token: $VAULT_TOKEN" \
#   $VAULT_ADDR/v1/secret/data/db_password

# O usando bibliotecas cliente (Python, Go, Node.js, etc.)
# pip install hvac
import hvac
client = hvac.Client(url='http://vault:8200', token='root-token')
secret = client.secrets.kv.v1.read_secret('db_password')['data']['value']

6. Actualizaciones sin Downtime

Actualizar un servicio en producción no debería significar tiempo de inactividad. Docker Compose y Swarm ofrecen mecanismos integrados para rolling updates, y con Docker Compose puedes implementar estrategias blue-green manualmente.

Health Checks

Un health check le dice a Docker cuándo un contenedor está realmente listo para recibir tráfico. Sin esto, los despliegues reemplazan contenedores antes de que la aplicación esté operativa.

# Health check en Dockerfile
FROM node:20-alpine
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "index.js"]
# Health check en docker-compose (sobrescribe el del Dockerfile)
services:
  app:
    image: mi-app:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
    deploy:
      replicas: 3
      update_config:
        order: start-first
        failure_action: rollback

Rolling Updates con Docker Compose

# docker-compose.yml con rolling updates
services:
  web:
    image: mi-app:${TAG}
    deploy:
      replicas: 5
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
        failure_action: rollback
        monitor: 30s
      rollback_config:
        parallelism: 1
        order: stop-first
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
# Despliegue gradual
export TAG=v2.1.0
docker stack deploy -c docker-compose.yml mi-stack

# Ver progreso del rolling update
docker service ps mi-stack_web

# Forzar rollback si algo sale mal
docker service update --rollback mi-stack_web

Blue-Green Deployment

Con Docker Compose puedes implementar blue-green usando dos stacks y un proxy inverso como Nginx o Traefik que conmute el tráfico.

# Blue (producción activa)
services:
  app-blue:
    image: mi-app:1.0.0
    container_name: app-blue
    ports:
      - "3001:3000"
    networks:
      - app-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]

  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx-blue.conf:/etc/nginx/conf.d/default.conf:ro
    ports:
      - "80:80"
    depends_on:
      - app-blue

networks:
  app-net:
# Green (nueva versión)
services:
  app-green:
    image: mi-app:2.0.0
    container_name: app-green
    ports:
      - "3002:3000"
    networks:
      - app-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]

Una vez que la versión green pasa todos los health checks, actualizas el upstream de Nginx para apuntar a app-green:3000 y recargas la configuración:

# docker exec nginx nginx -s reload

Graceful Shutdown con Señales

Cuando Docker detiene un contenedor, envía SIGTERM y espera un tiempo (por defecto 10s) antes de enviar SIGKILL. Tu aplicación debe manejar SIGTERM para cerrar conexiones activas de forma limpia.

// Manejo de SIGTERM en Node.js
const server = app.listen(3000);

process.on('SIGTERM', () => {
  console.log('SIGTERM recibido. Cerrando conexiones...');
  server.close(() => {
    console.log('Servidor detenido.');
    process.exit(0);
  });
});
# Aumentar el tiempo de gracia en docker-compose
services:
  app:
    image: mi-app:latest
    stop_grace_period: 60s
    stop_signal: SIGTERM

7. Backup y Restauración de Volúmenes

Los volúmenes Docker almacenan datos de bases de datos, colas, archivos subidos y otros estados críticos. Perderlos puede ser catastrófico. Aquí tienes scripts prácticos para backup y restore.

Backup de un Volumen Docker

#!/bin/bash
# backup-volume.sh — Backup de un volumen Docker a un archivo tar.gz

VOLUME_NAME=$1
BACKUP_DIR=${2:-./backups}
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${VOLUME_NAME}_${TIMESTAMP}.tar.gz"

mkdir -p "$BACKUP_DIR"

docker run --rm \
  -v "${VOLUME_NAME}":/data:ro \
  -v "$(pwd)/${BACKUP_DIR}":/backup \
  alpine:latest \
  tar czf "/backup/$(basename ${BACKUP_FILE})" -C /data .

echo "✅ Backup completado: ${BACKUP_FILE}"
# Uso
chmod +x backup-volume.sh
./backup-volume.sh postgres_data
./backup-volume.sh redis_data ./mis-backups

Backup Automatizado con Cron

#!/bin/bash
# automated-backup.sh — Backup automático con rotación

VOLUMES=("postgres_data" "redis_data" "app_uploads")
BACKUP_DIR="/backups/docker"
RETENTION_DAYS=30
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

for vol in "${VOLUMES[@]}"; do
    echo "Respaldando volumen: $vol"
    docker run --rm \
        -v "${vol}":/data:ro \
        -v "${BACKUP_DIR}":/backup \
        alpine:latest \
        tar czf "/backup/${vol}_${TIMESTAMP}.tar.gz" -C /data .

    # Verificar que el backup se creó
    if [ -f "${BACKUP_DIR}/${vol}_${TIMESTAMP}.tar.gz" ]; then
        echo "  ✅ ${vol} respaldado correctamente"
    else
        echo "  ❌ Error al respaldar ${vol}"
    fi
done

# Rotación: eliminar backups más antiguos que RETENTION_DAYS
find "${BACKUP_DIR}" -name "*.tar.gz" -type f -mtime +${RETENTION_DAYS} -delete
echo "🗑️  Rotación completada. Backups antiguos (>${RETENTION_DAYS} días) eliminados."
# Programar en crontab (se ejecuta diario a las 3am)
# 0 3 * * * /path/to/automated-backup.sh >> /var/log/docker-backup.log 2>&1

Restauración de un Volumen

#!/bin/bash
# restore-volume.sh — Restaura un volumen Docker desde un archivo tar.gz

VOLUME_NAME=$1
BACKUP_FILE=$2

if [ -z "$VOLUME_NAME" ] || [ -z "$BACKUP_FILE" ]; then
    echo "Uso: $0  "
    exit 1
fi

if [ ! -f "$BACKUP_FILE" ]; then
    echo "❌ El archivo de backup no existe: ${BACKUP_FILE}"
    exit 1
fi

# Verificar si el volumen existe; si no, crearlo
docker volume inspect "$VOLUME_NAME" &>/dev/null || docker volume create "$VOLUME_NAME"

echo "Restaurando ${VOLUME_NAME} desde ${BACKUP_FILE}..."

docker run --rm \
  -v "${VOLUME_NAME}":/data \
  -v "$(pwd)/${BACKUP_FILE}":/backup/restore.tar.gz:ro \
  alpine:latest \
  tar xzf /backup/restore.tar.gz -C /data

echo "✅ Restauración completada: ${VOLUME_NAME}"
# Uso
./restore-volume.sh postgres_data ./backups/postgres_data_20240315_030000.tar.gz

Backup de Bases de Datos con pg_dump

Para bases de datos, un backup a nivel de archivo del volumen no siempre es seguro. Usa las herramientas nativas de la base de datos:

#!/bin/bash
# backup-postgres.sh — Backup lógico de PostgreSQL

CONTAINER_NAME="postgres-prod"
DB_NAME="myapp"
DB_USER="admin"
BACKUP_DIR="./db-backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

mkdir -p "$BACKUP_DIR"

docker exec "$CONTAINER_NAME" \
  pg_dump -U "$DB_USER" -d "$DB_NAME" \
  --format=custom \
  --file="/tmp/${DB_NAME}_${TIMESTAMP}.dump" \
  --verbose

docker cp "${CONTAINER_NAME}:/tmp/${DB_NAME}_${TIMESTAMP}.dump" \
  "${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.dump"

docker exec "$CONTAINER_NAME" rm "/tmp/${DB_NAME}_${TIMESTAMP}.dump"

echo "✅ Backup de PostgreSQL completado: ${DB_NAME}_${TIMESTAMP}.dump"

8. Hardening del Host Docker

Un contenedor es tan seguro como el host que lo ejecuta. Hardening del host Docker significa configurar el daemon, el kernel y los mecanismos de seguridad de Linux para minimizar la superficie de ataque.

Configuración del Daemon Docker

El archivo /etc/docker/daemon.json permite configurar decenas de opciones de seguridad. Esta es una configuración recomendada para producción:

{
  "icc": false,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "live-restore": true,
  "userland-proxy": false,
  "iptables": true,
  "ip-forward": true,
  "ip-masq": true,
  "bridge": "none",
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 64000,
      "Soft": 64000
    }
  },
  "experimental": false,
  "authorization-plugins": [],
  "tls": true,
  "tlsverify": true,
  "tlscacert": "/etc/docker/ca.pem",
  "tlscert": "/etc/docker/server-cert.pem",
  "tlskey": "/etc/docker/server-key.pem"
}

Nota: live-restore: true mantiene los contenedores en ejecución incluso si el daemon Docker se reinicia. icc: false deshabilita la comunicación entre contenedores en la red bridge por defecto.

User Namespaces (userns-remap)

User namespaces re-mapean el usuario root del contenedor (UID 0) a un usuario no privilegiado en el host (por ejemplo, UID 100000). Así, incluso si un atacante escapa del contenedor como root, en el host es un usuario sin privilegios.

# /etc/docker/daemon.json — habilitar user namespaces
{
  "userns-remap": "default"
}
# Al usar userns-remap, los volúmenes bind mount pueden tener
# problemas de permisos. Solución: usa el UID re-mapeado
# ── El usuario nobody (65534) en el contenedor se mapea a
#     un UID alto en el host.
# ── Para bind mounts, ajusta el propietario en el host:
#     chown -R 165534:165534 ./data/

Perfiles Seccomp

Seccomp (Secure Computing Mode) restringe las llamadas al sistema que un contenedor puede hacer. Docker incluye un perfil por defecto que bloquea ~44 syscalls peligrosas. Puedes crear perfiles más restrictivos.

# Perfil seccomp personalizado: bloquear mount y unshare
echo '{
  "defaultAction": "SCMP_ACT_ALLOW",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": ["mount", "umount2", "unshare", "chroot", "pivot_root"],
      "action": "SCMP_ACT_ERRNO"
    },
    {
      "names": ["ptrace", "perf_event_open", "bpf"],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}' > /etc/docker/seccomp-hardened.json
# Usar perfil seccomp personalizado
docker run --rm \
  --security-opt seccomp=/etc/docker/seccomp-hardened.json \
  nginx:alpine

# En docker-compose
services:
  app:
    image: mi-app:latest
    security_opt:
      - seccomp:/etc/docker/seccomp-hardened.json

AppArmor

AppArmor es un módulo de seguridad de Linux (LSM) que asigna perfiles de seguridad a programas. Docker se integra con AppArmor en sistemas Debian/Ubuntu.

# Ver perfil AppArmor por defecto de Docker
cat /etc/apparmor.d/docker

# Perfil AppArmor personalizado (/etc/apparmor.d/docker-custom)
#include 
profile docker-custom flags=(attach_disconnected,mediate_deleted) {
  #include 
  #include 

  network,
  capability,
  file,

  deny /etc/shadow w,
  deny /etc/gshadow w,
  deny @{HOME}/.ssh/ rw,
  deny /proc/*/mem rw,
  deny /sys/fs/cgroup/ rw,
}
# Cargar y usar perfil AppArmor
sudo apparmor_parser -r -W /etc/apparmor.d/docker-custom

docker run --rm \
  --security-opt apparmor=docker-custom \
  mi-app:latest

Resource Limits con Cgroups

Los cgroups (control groups) limitan y aíslan el uso de recursos. Sin límites, un contenedor puede consumir toda la memoria del host y provocar un OOM Kill en otros procesos.

# Límites estrictos con docker run
docker run --rm \
  --memory=512m \
  --memory-reservation=256m \
  --memory-swap=768m \
  --cpus=1.5 \
  --cpuset-cpus=0,1 \
  --pids-limit=100 \
  --blkio-weight=500 \
  mi-app:latest
# docker-compose con límites completos
services:
  app:
    image: mi-app:latest
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M
    oom_kill_disable: false
    memswap_limit: 768M
    pids_limit: 100

Buenas Prácticas Resumidas

Conclusión

Docker en producción no es solo docker run. Hemos recorrido ocho áreas críticas: seguridad, monitoreo, logging centralizado, redes avanzadas, gestión de secretos, actualizaciones sin downtime, backup de volúmenes y hardening del host. Cada una de estas disciplinas convierte un despliegue frágil en un sistema robusto, observable y seguro.

No intentes implementarlo todo el primer día. Empieza por los health checks y límites de recursos, luego añade monitoreo, después logs centralizados, y así sucesivamente. La madurez de operaciones se construye por capas, igual que tus imágenes Docker.

Recuerda: en producción, lo barato sale caro. Invertir tiempo en estas prácticas desde el principio te ahorrará madrugadas apagando incendios.