DEVOPS 10 min

Docker para Bases de Datos: Despliegue en Producción de MySQL con Docker Compose

Guía completa para operar bases de datos MySQL en contenedores: arquitectura de volúmenes persistentes, mitigación de la trampa de iptables, inyección de secretos con Docker Secrets y backups en caliente con mysqldump.

Escrito por Samir El Azem Crovetto • Senior Backend Engineer & DevSecOps

El Reto de las Cargas con Estado (Stateful) en Contenedores

La contenedorización con Docker se concibió inicialmente para aplicaciones sin estado (stateless): APIs web, microservicios y trabajadores en segundo plano que pueden destruirse, escalarse horizontalmente o reiniciarse en cualquier nodo del clúster sin pérdida de información.

Sin embargo, cuando trasladamos este paradigma a motores de bases de datos relacionales (stateful) como MySQL, MariaDB o PostgreSQL, las reglas del juego cambian radicalmente. Un error de diseño en la gestión de volúmenes, una asignación incorrecta de permisos de usuario o una exposición descuidada de puertos de red pueden resultar en una catástrofe operativa: corrupción de datos por fallos de sincronización de disco (fsync), pérdida íntegra de la base de datos al recrear un contenedor o exposición directa del servicio a escáneres de todo Internet.

Desplegar bases de datos en contenedores en entornos de producción no solo es viable, sino sumamente potente para garantizar entornos reproducibles, aislados y automatizados, siempre que se respeten con rigor los principios de persistencia de almacenamiento, aislamiento perimetral, gobernanza de recursos y gestión de secretos.

En esta guía técnica analizamos la arquitectura completa para operar MySQL en producción mediante Docker Compose: desde la anatomía de los volúmenes nombrados y la mitigación de la trampa de iptables, hasta la configuración de comprobaciones de salud (healthchecks), la inyección segura de credenciales y los procedimientos de respaldo en caliente.


Arquitectura de Red Segura y Persistencia en Docker Compose

El siguiente diagrama detalla la topología de red aislada y el flujo de almacenamiento para un despliegue de base de datos en producción:

flowchart TD
    subgraph Public_Traffic ["Trafico Externo / Internet"]
        U[Usuarios Web / Clientes] -->|HTTPS 443| RP[Reverse Proxy / Nginx / Caddy]
    end

    subgraph Docker_Bridge_Networks ["Redes Internas de Docker"]
        subgraph Frontend_Net ["frontend_network (Aislada)"]
            RP -->|Proxy Pass| APP[API Backend / Spring Boot / Node.js]
        end

        subgraph Backend_Net ["backend_network (Completamente Privada)"]
            APP -->|Conexion TCP 3306 Interna| DB[(MySQL 8.0 / 8.4 LTS<br/>UID 999 non-root)]
            SEC[Docker Secrets<br/>/run/secrets/db_password] -.->|Inyeccion en Memoria| DB
        end
    end

    subgraph Host_Storage ["Almacenamiento Persistente en Host"]
        DB -->|I/O Directo fsync| VOL[(Docker Named Volume<br/>mysql_production_data)]
        DB -->|Rotacion de Binlogs| BLOG[(Volume: mysql_binlogs)]
    end

    classDef public fill:#181825,stroke:#f38ba8,stroke-width:2px,color:#cdd6f4;
    classDef network fill:#1e1e2e,stroke:#6366f1,stroke-width:2px,color:#cdd6f4;
    classDef storage fill:#181825,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4;
    class Public_Traffic public;
    class Docker_Bridge_Networks,Frontend_Net,Backend_Net network;
    class Host_Storage storage;

1. Persistencia de Datos: Volúmenes Nombrados vs. Bind Mounts

La primera decisión arquitectónica es determinar dónde y cómo persistirán las tablas y archivos de InnoDB (/var/lib/mysql).

En Docker existen dos mecanismos principales para conectar el almacenamiento del host con el contenedor:

  1. Host Bind Mounts (/srv/mysql:/var/lib/mysql): Vinculan un directorio arbitrario del host. Aunque resultan intuitivos, presentan inconvenientes severos en producción: delegan la gestión de permisos (UID/GID) al sistema operativo anfitrión, son sensibles a cambios de permisos accidentales y pueden degradar el rendimiento de E/S en entornos virtualizados.
  2. Volúmenes Nombrados (named volumes): Son gestionados íntegramente por el demonio de Docker bajo /var/lib/docker/volumes/. Proporcionan mejor rendimiento de E/S, conservan los permisos nativos del usuario interno del contenedor (mysql con UID 999) y aíslan el almacenamiento de manipulaciones directas en el host.

Parámetros de Rendimiento y Durabilidad para InnoDB

Al ejecutar MySQL sobre almacenamiento de contenedores, la configuración del motor de almacenamiento en /etc/mysql/conf.d/custom.cnf debe equilibrar la integridad transaccional (ACID) y la latencia de disco:

  • innodbflushmethod = O_DIRECT: Evita el doble almacenamiento en búfer (double buffering), impidiendo que tanto el kernel de Linux como MySQL almacenen en memoria caché las mismas páginas de datos.
  • innodbflushlogattrx_commit = 1: Garantiza la durabilidad estricta según ACID, forzando la escritura y descarga a disco (fsync) del log de transacciones en cada commit.
  • innodbbufferpool_size: Debe asignarse aproximadamente entre el 60% y el 70% de la memoria total reservada para el contenedor en los límites de Docker Compose.

2. Aislamiento Perimetral y la Trampa de iptables en Docker

Uno de los errores de seguridad más graves y comunes en la administración de contenedores es la exposición involuntaria de bases de datos a redes públicas.

La Trampa de Docker con el Firewall del Host (UFW / iptables)

En los sistemas Linux (como Ubuntu o Debian), los administradores configuran herramientas como UFW (Uncomplicated Firewall) para denegar el tráfico entrante por defecto. Sin embargo, cuando se define una directiva como:

ports:
  - "3306:3306"

El demonio de Docker manipula directamente las cadenas de PREROUTING de iptables insertando reglas en la cadena DOCKER antes de que UFW pueda evaluar el paquete. Como resultado, el puerto 3306 queda expuesto a todo Internet, saltándose por completo las políticas del firewall local.

La Mitigación Arquitectónica:

Para entornos de producción, existen dos aproximaciones seguras:

  1. Sin publicación de puertos (Recomendado): La base de datos y los microservicios comparten una red privada interna (backend_network). La aplicación se comunica con la base de datos resolviendo su nombre de servicio DNS interno (mysql-db:3306). Ningún puerto se publica hacia el host.
  2. Binding estricto a la interfaz de loopback: Si un administrador necesita acceso directo para tareas de mantenimiento o túneles SSH, el puerto debe vincularse explícitamente a 127.0.0.1:
   ports:
     - "127.0.0.1:3306:3306"

De este modo, las conexiones solo pueden originarse localmente o a través de un túnel cifrado, tal como detallamos en nuestra guía sobre Hardening de Servidores SSH y Bastiones.


3. Gestión Segura de Credenciales con Docker Secrets

Almacenar contraseñas en texto plano dentro del archivo docker-compose.yml (por ejemplo, MYSQLROOTPASSWORD: secret123) es una vulnerabilidad crítica: las credenciales quedan expuestas en el historial de Git, en los registros de auditoría y ante cualquier usuario con permisos de lectura sobre el repositorio.

La solución estándar en Docker Compose consiste en emplear Docker Secrets o archivos de secretos inyectados en memoria:

secrets:
  db_root_password:
    file: ./secrets/db_root_password.txt
  db_app_password:
    file: ./secrets/db_app_password.txt

La imagen oficial de MySQL soporta la terminación _FILE en sus variables de entorno. Al declarar:

environment:
  MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
  MYSQL_PASSWORD_FILE: /run/secrets/db_app_password

El contenedor lee la contraseña directamente desde el sistema de archivos en memoria virtual (/run/secrets/), impidiendo que el secreto quede registrado en variables de entorno inspeccionables mediante docker inspect.


4. Manifiesto Completo de Producción: docker-compose.yml

A continuación se presenta un manifiesto de producción endurecido, con segmentación de redes, comprobación de salud (healthcheck), límites de recursos cgroups y gestión de secretos:

# docker-compose.yml - Produccion Hardened MySQL 8.0 / 8.4 LTS
version: '3.8'

services:
  mysql-db:
    image: mysql:8.0.36
    container_name: production_mysql
    restart: unless-stopped
    # Ejecucion sin privilegios root dentro del contenedor
    user: "999:999"
    
    # Inyeccion de secretos desde archivos protegidos
    secrets:
      - db_root_password
      - db_app_password
      
    environment:
      MYSQL_DATABASE: corporate_prod
      MYSQL_USER: app_service
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
      MYSQL_PASSWORD_FILE: /run/secrets/db_app_password
      
    # Limites estrictos de hardware (cgroups v2)
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2048M
        reservations:
          cpus: '0.5'
          memory: 1024M

    # Comprobacion de salud nativa para arranque sincronizado
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u $MYSQL_USER --password=$(cat /run/secrets/db_app_password) --silent"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

    # Solo acceso local al host (para tuneles SSH administrativos)
    ports:
      - "127.0.0.1:3306:3306"

    # Persistencia de datos, inicializacion y configuracion
    volumes:
      - mysql_data:/var/lib/mysql
      - mysql_logs:/var/log/mysql
      - ./init-scripts:/docker-entrypoint-initdb.d:ro
      - ./config/custom.cnf:/etc/mysql/conf.d/custom.cnf:ro

    # Red privada exclusiva para comunicacion con servicios internos
    networks:
      - backend_network

  api-service:
    image: registry.empresa.com/services/core-api:1.2.0
    container_name: core_api_service
    restart: unless-stopped
    depends_on:
      mysql-db:
        condition: service_healthy
    networks:
      - backend_network
      - frontend_network
    environment:
      DB_HOST: mysql-db
      DB_PORT: 3306
      DB_NAME: corporate_prod
      DB_USER: app_service

volumes:
  mysql_data:
    name: prod_mysql_data
  mysql_logs:
    name: prod_mysql_logs

networks:
  backend_network:
    name: prod_backend_network
    internal: true # Bloquea cualquier trafico entrante o saliente de Internet
  frontend_network:
    name: prod_frontend_network

secrets:
  db_root_password:
    file: ./secrets/db_root_password.txt
  db_app_password:
    file: ./secrets/db_app_password.txt

5. Inicialización Idempotente con docker-entrypoint-initdb.d

Las imágenes oficiales de MySQL ejecutan automáticamente cualquier script .sh, .sql o .sql.gz ubicado en /docker-entrypoint-initdb.d/ exclusivamente la primera vez que se inicializa el volumen de datos.

Para que la automatización sea resiliente, todos los scripts de inicialización deben escribirse de forma idempotente (utilizando sentencias CREATE TABLE IF NOT EXISTS, índices condicionales y migraciones seguras):

-- init-scripts/01-init-schema.sql
CREATE TABLE IF NOT EXISTS security_audit_logs (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    event_type VARCHAR(64) NOT NULL,
    actor_id VARCHAR(128) NOT NULL,
    ip_address VARCHAR(45) NOT NULL,
    metadata JSON NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_actor_created (actor_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

6. Procedimiento de Respaldo en Caliente y Recuperación ante Desastres

Un contenedor sin una política de respaldos automatizada es un incidente de seguridad esperando ocurrir.

En bases de datos con motores transaccionales InnoDB, no es seguro copiar los archivos físicos de /var/lib/mysql mientras el motor está en ejecución, ya que los búferes en memoria no volcados causarán inconsistencias en la base de datos.

La práctica estándar consiste en generar volcados lógicos en caliente mediante mysqldump con la directiva --single-transaction, la cual establece un punto de lectura consistente sin bloquear las operaciones de escritura de la aplicación:

Script Automatizado de Respaldo y Cifrado (backup-mysql.sh)

#!/usr/bin/env bash
set -euo pipefail

# Configuracion de variables
CONTAINER_NAME="production_mysql"
DATABASE_NAME="corporate_prod"
BACKUP_DIR="/opt/backups/mysql"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/backup_${DATABASE_NAME}_${TIMESTAMP}.sql.gz"
SECRET_FILE="/opt/app/secrets/db_root_password.txt"
RETENTION_DAYS=14

mkdir -p "${BACKUP_DIR}"

# Extraer el password de root desde el secreto local
MYSQL_PWD=$(cat "${SECRET_FILE}")

echo "[$(date)] Iniciando backup en caliente para ${DATABASE_NAME}..."

# Ejecutar mysqldump dentro del contenedor y comprimir en streaming
docker exec -e MYSQL_PWD="${MYSQL_PWD}" "${CONTAINER_NAME}" \
  mysqldump \
    -u root \
    --single-transaction \
    --quick \
    --skip-lock-tables \
    --routines \
    --triggers \
    "${DATABASE_NAME}" | gzip -9 > "${BACKUP_FILE}"

# Restringir permisos del archivo de backup generado
chmod 600 "${BACKUP_FILE}"

echo "[$(date)] Backup completado con exito: ${BACKUP_FILE}"

# Depurar respaldos antiguos que superen la politica de retencion
find "${BACKUP_DIR}" -type f -name "backup_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
echo "[$(date)] Politica de retencion aplicada (${RETENTION_DAYS} dias)."

Restauración Verificada:

Para validar la recuperación de desastres en un entorno de pruebas:

# Restaurar un volcado comprimido hacia el contenedor de base de datos
gunzip < /opt/backups/mysql/backup_corporate_prod_20261027_040000.sql.gz | \
  docker exec -i -e MYSQL_PWD=$(cat ./secrets/db_root_password.txt) production_mysql \
  mysql -u root corporate_prod

Conclusión

El despliegue de bases de datos relacionales en contenedores Docker mediante Docker Compose proporciona una base sólida para arquitecturas modernas, siempre que se sustituya la improvisación por controles de ingeniería deterministas: aislamiento estricto de red sin exposición pública involuntaria, almacenamiento desacoplado en volúmenes nombrados, inyección de credenciales mediante secretos y respaldos transaccionales automatizados.

Este enfoque de infraestructura como código se alinea con las prácticas de Hardening de Servidores Linux, las directivas de Arquitectura y Seguridad en Docker y la gobernanza continua en Pipelines DevSecOps.

Si tu organización necesita diseñar arquitecturas de almacenamiento persistente en contenedores, auditar el aislamiento de sus redes Docker o establecer procedimientos de alta disponibilidad y recuperación ante desastres para sus bases de datos, puedes consultar mis Servicios de Ciberseguridad y Consultoría o ponerte en contacto conmigo a través de Sobre mí.