MySQL vs MariaDB en Docker: por qué «Funcionó localmente» falla en producción

¿Te fue útil?

El desarrollo local es un mentiroso amable. Tu portátil tiene un solo usuario, un NVMe rápido, una caché de sistema de ficheros caliente y una pila Docker Desktop que “amablemente” suaviza las aristas. Producción no tiene nada de eso. Producción tiene concurrencia real, latencia de E/S real, actualizaciones reales y los modos de fallo que llegan a las 03:12 con una invitación de calendario titulada “SEV-1”.

Si ejecutas MySQL o MariaDB en contenedores Docker, la brecha entre “arranca” y “sobrevive” es donde la mayoría de los equipos sangran. Los motores son similares—hasta que no lo son. Las imágenes parecen intercambiables—hasta que una versión menor cambia un valor por defecto. El almacenamiento es “solo un volumen”—hasta que fsync se convierte en tu nueva religión.

Qué se rompe en producción (y por qué)

Cuando alguien dice “MySQL y MariaDB son básicamente lo mismo”, lo que suele significar es: “mi aplicación ejecutó algunas consultas y no se cayó”. Producción exige garantías más fuertes: arranque predecible, valores por defecto estables, actualizaciones seguras, rendimiento consistente bajo carga y recuperabilidad cuando algo sale mal.

Las grandes categorías de fallo “funcionó localmente”

  • Autenticación y compatibilidad de cliente: MySQL 8 cambió valores por defecto que a clientes antiguos no les gustan. MariaDB siguió su propio camino.
  • Semántica del almacenamiento persistente: los bind mounts vs volúmenes nombrados vs volúmenes de red se comportan distinto con fsync, permisos y latencia.
  • Límites de recursos: tu portátil sobreasigna memoria y CPU. Los contenedores en producción reciben cgroups y aplicación de límites.
  • Orden de arranque: Compose “depends_on” no significa que el servicio esté listo. Tu app golpea la BD mientras aún realiza recuperación tras un fallo.
  • Deriva de configuración: en local usas valores por defecto; en prod usas un archivo de configuración; en staging usas una variable de entorno. Sorpresa: estás ejecutando tres bases de datos diferentes.
  • Rutas de actualización: un salto menor de imagen puede cambiar valores por defecto, formatos de redo log o comportamiento de replicación.
  • Diferencias de modo SQL/colaciones: las diferencias sutiles se manifiestan como “¿por qué prod rechaza este INSERT?”

Aquí está el tema: MySQL y MariaDB no son “solo contenedores”. Son sistemas con estado con aristas afiladas de durabilidad y rendimiento. Docker facilita iniciarlos; no facilita ejecutarlos.

Un chiste corto (1/2): Ejecutar una base de datos en un contenedor es como poner un piano en un patinete—es posible, pero más vale que te importe el suelo.

Adopta una postura: qué deberías hacer

Si vas a enviar a producción:

  • Fija versiones. Etiquetas explícitas. Nada de latest. Nunca.
  • Elige el motor intencionalmente. Si necesitas funciones de Oracle MySQL o compatibilidad estricta con el comportamiento de MySQL 8, usa MySQL. Si necesitas funcionalidades específicas de MariaDB u razones operativas (como ciertas distribuciones), usa MariaDB. No “lo cambies después”.
  • Diseña el almacenamiento primero. Decide dónde vive la data, cómo se respalda y cómo se restaura antes de enviar una sola consulta.
  • Mide la latencia de fsync. No IOPS de marketing. El comportamiento real de fsync en la ruta de almacenamiento real.

Hechos históricos interesantes (que sí usarás)

Estos no son datos para un trivial. Explican por qué los valores por defecto difieren, por qué la compatibilidad no está garantizada y por qué “reemplazo directo” tiene fecha de caducidad.

  1. MariaDB se creó como un fork de MySQL tras las adquisiciones de Sun/Oracle, para mantener un camino gobernado por la comunidad. Esa historia explica por qué los nombres son parecidos y la divergencia es… tanto política como técnica.
  2. MySQL 8 cambió el plugin de autenticación por defecto a caching_sha2_password, lo que rompió clientes antiguos y herramientas que esperaban mysql_native_password.
  3. MySQL 8 eliminó el query cache (era una trampa bajo concurrencia). Si dependías de él en MySQL 5.7, estabas confiando en una trampa.
  4. El optimizador y el ecosistema de motores de MariaDB divergieron con el tiempo (p. ej., Aria, MyRocks en ciertos contextos, y distintas líneas históricas de InnoDB/XtraDB). El efecto neto: “misma consulta” no significa “mismo plan”.
  5. La replicación GTID existe en ambos mundos, pero los detalles de implementación difieren lo suficiente como para que la replicación/migración entre motores necesite planificación, no intuición.
  6. Las imágenes oficiales de Docker son empaquetados opinados. Scripts de entrypoint, usuarios por defecto, permisos y lógica de inicialización forman parte del producto que ejecutas.
  7. Los sistemas de archivos y los controladores en Linux importan. Sistemas de archivos overlay, sistemas de archivos de red y capas copy-on-write influyen en el coste de fsync y el comportamiento de metadatos.
  8. La durabilidad es configurable. Ajustes como innodb_flush_log_at_trx_commit intercambian seguridad de datos por velocidad. Muchas guías “rápidas” para contenedores establecen discretamente la durabilidad como “opcional”.

MySQL vs MariaDB en contenedores: las diferencias que muerden

1) El comportamiento de la imagen es parte de tu BD

Cuando ejecutas mysql:8.0 o mariadb:11, no solo eliges un motor de base de datos. Estás eligiendo:

  • Cómo funciona la inicialización (comportamiento de /docker-entrypoint-initdb.d, valores por defecto de charset, manejo de la contraseña root).
  • Cómo se ve la configuración por defecto y desde dónde se carga.
  • Qué UID usa el servicio al ejecutarse, afectando permisos de volumen.

En desarrollo local, borras volúmenes sin remordimientos. En producción, tus scripts de entrypoint nunca deberían ser lo único entre “reinicio” y “directorio de datos inutilizable”.

2) Autenticación y TLS: no es solo “una contraseña”

El fallo clásico: la app usa un conector antiguo. En local corrías MariaDB o MySQL 5.7. En prod, alguien eligió MySQL 8 “porque es más nuevo”. La app empieza a fallar inicios de sesión con errores sobre plugins de autenticación o claves públicas RSA.

Toma una decisión: o actualizas las librerías cliente para soportar los valores por defecto de MySQL 8, o configuras explícitamente MySQL 8 para usar mysql_native_password para ese usuario (con los ojos abiertos respecto a la postura de seguridad). Para MariaDB, la historia de autenticación difiere lo suficiente como para validar la compatibilidad del conector desde temprano.

3) Modos SQL y colaciones: “funciona” hasta que alguien inserta datos reales

Los conjuntos de datos locales están limpios. Los datos de producción son un vertedero con valor comercial. Diferencias en:

  • sql_mode (rigidez en fechas inválidas, cadenas truncadas y conversiones implícitas)
  • conjunto de caracteres por defecto (p. ej., utf8mb4 vs el viejo utf8)
  • valores por defecto de colación (reglas de ordenación y comparación)

…pueden convertir comportamiento silencioso en local en una ruptura ruidosa en producción.

4) Rendimiento: misma consulta, distinto plan, distinto dolor

MySQL y MariaDB comparten mucho ADN, pero pueden elegir planes de ejecución distintos bajo carga. Docker añade su propio impuesto de rendimiento: comportamiento del filesystem overlay, drivers de almacenamiento y E/S limitadas que no aparecen en el portátil del desarrollador.

Si vas en serio: trata cada motor + versión como una plataforma separada. Mide el workload real que tienes, no un microbenchmark sintético.

5) Replicación y migración: “suficientemente compatible” no es una estrategia

Llegará un punto en que un equipo quiera migrar MariaDB a MySQL, o al revés, por contratos de soporte, ofertas de servicio gestionado o necesidades de características. La contenedorización no lo hace más fácil; lo hace más fácil de hacer de forma casual y, por tanto, peligrosa.

Los dumps lógicos (mysqldump) son lentos pero portables. Copias físicas de directorios de datos son rápidas pero frágiles entre versiones y motores. La replicación puede ser elegante pero exige disciplina de compatibilidad.

Almacenamiento en Docker: donde rendimiento y durabilidad discuten

Las bases de datos hacen dos cosas todo el día: leer y escribir. El almacenamiento en Docker determina si esas escrituras son rápidas, durables y recuperables—o “rápidas hasta que no lo son”, que es una forma educada de describir corrupción de datos y outages prolongados.

Volúmenes nombrados vs bind mounts: elige intencionalmente

  • Volumen nombrado: ruta gestionada por Docker, normalmente permisos estables, menos sorpresas. Operativamente más agradable para muchas configuraciones.
  • Bind mount: controlas la ruta exacta del host. Genial para integración de backups predecible y depuración, pero fácil de arruinar con propiedad, contextos SELinux/AppArmor y elecciones de sistema de archivos.

Si montas por enlace un directorio desde un sistema de archivos host con semánticas inusuales (FS de red, algunos sistemas distribuidos, NFS mal configurado), puedes tener paradas en fsync o riesgo de corrupción.

La latencia de fsync es tu impuesto “funcionó localmente”

En desarrollo local, fsync es rápido y la caché está caliente. En producción, fsync puede ser lento debido a:

  • almacenamiento en red
  • política de cache del controlador RAID
  • comportamiento de almacenamiento en bloque en la nube
  • sobrecoste de copy-on-write
  • contención a nivel del host

Cuando fsync es lento, los flushes de log de InnoDB son lentos. Luego los commits son lentos. Luego tu app revienta timeouts. Luego alguien “lo arregla” bajando la durabilidad. Y ahora estás apostando tu nómina.

Una cita (idea parafraseada): Idea parafraseada de Werner Vogels: todo falla, todo el tiempo—diseña para ello en lugar de esperar que no ocurra.

Límites de memoria del contenedor y comportamiento de InnoDB

InnoDB quiere memoria para buffer pool, log buffers y caches. Los contenedores tienen límites. Si no dimensionas innodb_buffer_pool_size y afines teniendo en cuenta cgroups, el kernel OOM killer eventualmente tomará una decisión por ti. No será la decisión que querías.

Comprobaciones de salud vs readiness

Un contenedor que está “arriba” no es necesariamente “listo”. Tras un crash o reinicio forzado, MySQL/MariaDB pueden realizar recuperación por fallo. El proceso acepta conexiones TCP pronto, pero las consultas pueden bloquearse o fallar hasta que la recuperación termine. Aquí es donde la orquestación ingenua convierte un incidente transitorio en un fallo en cascada.

Tareas prácticas: comandos, salidas y qué decisiones tomar

A continuación hay tareas de nivel producción que puedes ejecutar hoy. Cada una incluye: comando, una salida realista de muestra, qué significa y la decisión que debes tomar a partir de ella. Haz estas antes de declarar la victoria.

Tarea 1: Confirma qué motor y versión estás realmente ejecutando

cr0x@server:~$ docker exec -it db mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT @@version, @@version_comment;"
+-----------+----------------------------------+
| @@version | @@version_comment                |
+-----------+----------------------------------+
| 8.0.36    | MySQL Community Server - GPL     |
+-----------+----------------------------------+

Qué significa: Esto es Oracle MySQL 8.0.36, no MariaDB. El comentario de versión importa; MariaDB imprime su propia marca.

Decisión: Usa documentación específica del motor y notas de migración. No apliques ajustes o suposiciones de MariaDB a MySQL 8.

Tarea 2: Comprueba la etiqueta y el digest de la imagen del contenedor (pinning)

cr0x@server:~$ docker inspect --format '{{.Config.Image}} {{index .RepoDigests 0}}' db
mysql:8.0 mysql@sha256:3a5f2c8e7f2c2c6f2d2f4c1c0b0b7c6d9e8f1a2b3c4d5e6f7a8b9c0d1e2f3a4

Qué significa: Estás ejecutando una etiqueta y hay un digest específico. Las etiquetas se mueven; los digests no.

Decisión: Pinea a un digest para rollouts en producción (o al menos fija la versión menor y controla las actualizaciones).

Tarea 3: Ver si estás usando un volumen nombrado o bind mount para los datos

cr0x@server:~$ docker inspect db --format '{{json .Mounts}}'
[{"Type":"volume","Name":"prod-mysql-data","Source":"/var/lib/docker/volumes/prod-mysql-data/_data","Destination":"/var/lib/mysql","Driver":"local","Mode":"z","RW":true,"Propagation":""}]

Qué significa: Un volumen nombrado de Docker respalda /var/lib/mysql.

Decisión: Asegura que tus procedimientos de backup y restore sepan dónde vive ese volumen, y que el almacenamiento del host sea adecuado (sistema de archivos, latencia, monitorización).

Tarea 4: Verifica tipo de sistema de archivos y opciones de montaje bajo el directorio de datos

cr0x@server:~$ docker exec -it db bash -lc "df -T /var/lib/mysql && mount | grep ' /var/lib/mysql ' -n || true"
Filesystem     Type 1K-blocks      Used Available Use% Mounted on
/dev/nvme0n1p2 ext4  96143228  31287124  59921844  35% /var/lib/mysql

Qué significa: Los datos están en ext4; buena base. Si ves overlayfs o un montaje NFS aquí, para y replantea.

Decisión: Evita poner datos de InnoDB en sistemas de archivos de red a menos que entiendas completamente durabilidad y semánticas de bloqueo.

Tarea 5: Confirma el directorio de datos y ajustes críticos de durabilidad de InnoDB

cr0x@server:~$ docker exec -it db mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES WHERE Variable_name IN ('datadir','innodb_flush_log_at_trx_commit','sync_binlog');"
+------------------------------+----------------+
| Variable_name                | Value          |
+------------------------------+----------------+
| datadir                      | /var/lib/mysql/|
| innodb_flush_log_at_trx_commit | 1            |
| sync_binlog                  | 1              |
+------------------------------+----------------+

Qué significa: Durabilidad completa: flush de logs en cada commit, sincronización del binlog en cada commit.

Decisión: Mantén esto para producción a menos que hayas aceptado explícitamente tolerancia a pérdida de datos en caso de fallo. Si el rendimiento es malo, arregla almacenamiento y configuración antes de bajar estos valores.

Tarea 6: Comprueba los plugins de autenticación por defecto y la configuración de usuarios (problema clásico de MySQL 8)

cr0x@server:~$ docker exec -it db mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT user, host, plugin FROM mysql.user WHERE user IN ('app','root');"
+------+-----------+-----------------------+
| user | host      | plugin                |
+------+-----------+-----------------------+
| app  | %         | caching_sha2_password |
| root | localhost | caching_sha2_password |
+------+-----------+-----------------------+

Qué significa: El usuario app usa caching_sha2_password. Clientes antiguos pueden fallar.

Decisión: Actualiza el conector cliente o cambia el plugin por usuario (y prueba). No “arregles” esto degradando el servidor sin un plan.

Tarea 7: Valida el modo SQL y detecta deriva local/prod

cr0x@server:~$ docker exec -it db mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SELECT @@sql_mode\G"
*************************** 1. row ***************************
@@sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

Qué significa: Están habilitados modos estrictos. Aplicaciones que dependían de comportamiento permisivo pueden romperse.

Decisión: Prefiere arreglar la aplicación y el esquema en lugar de aflojar globalmente el modo SQL. Si debes ajustar, hazlo explícito y documentado.

Tarea 8: Comprueba el conjunto de caracteres y la colación por defecto (corrección de datos, no cosmética)

cr0x@server:~$ docker exec -it db mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'collation_server';"
+----------------------+---------+
| Variable_name        | Value   |
+----------------------+---------+
| character_set_server | utf8mb4 |
+----------------------+---------+
+------------------+--------------------+
| Variable_name    | Value              |
+------------------+--------------------+
| collation_server | utf8mb4_0900_ai_ci |
+------------------+--------------------+

Qué significa: MySQL 8 usa colaciones más nuevas por defecto. MariaDB difiere. Ordenación/comparación puede cambiar entre entornos.

Decisión: Estandariza charset/colación en migraciones de esquema. No lo dejes a los valores por defecto del servidor.

Tarea 9: Inspecciona logs del contenedor por recuperación tras fallo, problemas de permisos y fallos de plugins

cr0x@server:~$ docker logs --tail=80 db
2025-12-31T00:11:21.443915Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.36) initializing of server in progress as process 1
2025-12-31T00:11:22.119203Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2025-12-31T00:11:27.991130Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
2025-12-31T00:11:28.250984Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections.

Qué significa: Arranque limpio. Si ves “permission denied”, “upgrade after crash” o reinicios repetidos, trátalo como un problema de almacenamiento/configuración, no como un problema de la app.

Decisión: Haz que las comprobaciones de readiness reflejen la usabilidad real (por ejemplo, una consulta simple) e investiga recuperación lenta si se repite.

Tarea 10: Confirma límites de recursos (cgroups) para evitar OOMs misteriosos

cr0x@server:~$ docker inspect db --format 'Memory={{.HostConfig.Memory}} NanoCpus={{.HostConfig.NanoCpus}}'
Memory=2147483648 NanoCpus=2000000000

Qué significa: Límite de memoria de 2 GiB y cuota equivalente a 2 CPU están configurados.

Decisión: Ajusta buffer pool y límites de conexiones para que encajen. Si ejecutas MySQL con valores por defecto ilimitados dentro de una caja de 2 GiB, el OOM killer te dará una fiesta sorpresa.

Tarea 11: Observa memoria en tiempo de ejecución y busca historial de OOM kill

cr0x@server:~$ docker stats --no-stream db
CONTAINER ID   NAME   CPU %     MEM USAGE / LIMIT     MEM %     NET I/O        BLOCK I/O    PIDS
a1b2c3d4e5f6   db     185.23%   1.92GiB / 2.00GiB     96.00%    1.2GB / 1.1GB  9.8GB / 14GB  61

Qué significa: Estás coqueteando con el límite de memoria. Así es como se obtienen OOM kills durante picos.

Decisión: Reduce max_connections, ajusta el buffer pool o aumenta memoria. También evalúa patrones de consultas que causan los picos.

Tarea 12: Comprueba el dimensionamiento del buffer pool de InnoDB vs memoria del contenedor

cr0x@server:~$ docker exec -it db mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
+-------------------------+-----------+
| Variable_name           | Value     |
+-------------------------+-----------+
| innodb_buffer_pool_size | 1073741824|
+-------------------------+-----------+

Qué significa: El buffer pool es 1 GiB. En un contenedor de 2 GiB, esto puede ser razonable—si el resto (conexiones, tablas temporales, buffers de ordenación) está controlado.

Decisión: Mantén un margen. Si estás rutinariamente al 90% de memoria, reduce buffer pool o arregla el uso de memoria por conexión.

Tarea 13: Mide consultas lentas y si estás ligado por CPU o I/O

cr0x@server:~$ docker exec -it db mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW GLOBAL STATUS LIKE 'Slow_queries'; SHOW VARIABLES LIKE 'slow_query_log';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Slow_queries  | 1842  |
+---------------+-------+
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| slow_query_log | ON    |
+----------------+-------+

Qué significa: El contador de consultas lentas está subiendo. El logging está activado, así que puedes inspeccionar y arreglar.

Decisión: Extrae logs de lentas, arregla índices/patrones de consulta y verifica que producción use los mismos planes de consulta que staging.

Tarea 14: Valida el binlog y su formato (replicación y recuperación punto en el tiempo)

cr0x@server:~$ docker exec -it db mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'binlog_format';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin       | ON    |
+---------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+

Qué significa: Los binlogs están activados y en formato ROW—generalmente el formato de replicación más seguro para la corrección.

Decisión: Si quieres recuperación punto-en-el-tiempo, mantiene binlogs y envíalos. Si no, sé honesto: tu plan de recuperación es “restaurar el último backup y perder datos”.

Tarea 15: Confirma que tu app no usa por accidente supuestos de localhost/socket

cr0x@server:~$ docker exec -it app bash -lc "getent hosts db && nc -vz db 3306"
172.20.0.3      db
Connection to db 3306 port [tcp/mysql] succeeded!

Qué significa: El contenedor app puede resolver y alcanzar la BD por nombre de servicio sobre TCP.

Decisión: Estandariza en TCP para conectividad entre contenedores a menos que tengas una razón deliberada para compartir un socket UNIX vía volumen (raro y frágil).

Tarea 16: Comprueba corrupción de tablas o advertencias de recuperación tras fallo (salud de InnoDB)

cr0x@server:~$ docker exec -it db mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,80p'
*************************** 1. row ***************************
  Type: InnoDB
  Name:
Status:
=====================================
2025-12-31 00:22:10 0x7f1a2c1ff700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 44 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 102 srv_active, 0 srv_shutdown, 188 srv_idle
srv_master_thread log flush and writes: 290

Qué significa: Puedes inspeccionar por “LATEST DETECTED DEADLOCK” y esperas de E/S. La ausencia de errores visibles es buena; la presencia de esperas largas apunta a latencia de almacenamiento o contención.

Decisión: Si ves altas esperas de “log i/o” o bloqueos de “fsync”, investiga primero la latencia del almacenamiento.

Guion de diagnóstico rápido

Tienes un incidente. La app está lenta o caída. La gente escribe “alguna novedad?” en chat como si fuera una prueba de carga. Aquí está el orden que encuentra el cuello de botella rápidamente.

Primero: ¿La base de datos está realmente sana y lista?

  • Comprueba reinicios del contenedor y logs por bucles de recuperación, errores de permisos y mensajes de actualización.
  • Ejecuta una consulta trivial desde dentro del contenedor: SELECT 1; Si eso bloquea, no estás “arriba”.
  • Confirma espacio en disco y presión de inodos en la ruta del host que respalda el volumen.

Segundo: ¿Es latencia de I/O (fsync) o contención de CPU?

  • Si los commits son lentos y la concurrencia es alta, sospecha latencia de fsync y almacenamiento.
  • Si la CPU está al máximo y las consultas son lentas, sospecha índices faltantes, planes malos o demasiados hilos.
  • Si la memoria está cerca del límite, sospecha swapping (host) o OOM kills (contenedor).

Tercero: ¿Es deriva de configuración o regresión de compatibilidad?

  • Compara sql_mode, charset/colación y plugins de autenticación entre local/staging/prod.
  • Confirma etiqueta y digest de imagen. ¿Hubo un redeploy “inocente”?
  • Revisa versiones de conectores cliente y requisitos TLS.

Cuarto: ¿Es la ruta de red y resolución de nombres?

  • Valida descubrimiento de servicios dentro de la red Docker.
  • Confirma mapeos de puertos y reglas de firewall al cruzar hosts.
  • Busca tormentas de conexiones efímeras (mala configuración de pools).

Errores comunes: síntomas → causa raíz → corrección

1) Síntoma: La app no puede iniciar sesión tras migrar a MySQL 8

Causa raíz: Desajuste de plugin de autenticación (caching_sha2_password vs expectativas de clientes antiguos).

Corrección: Actualiza el conector cliente; o establece el plugin del usuario a mysql_native_password y vuelve a emitir la contraseña; luego programa una actualización adecuada.

2) Síntoma: “permission denied” aleatorio en el arranque después de redeploy

Causa raíz: Propiedad del volumen/labels SELinux no coinciden con el UID/GID de ejecución del contenedor; la ruta del bind mount fue creada por root en el host.

Corrección: Estandariza la creación de volúmenes; aplica propiedad correcta en la ruta del host; usa contexto de seguridad consistente; evita creación ad-hoc manual de directorios.

3) Síntoma: Las escrituras son lentas solo en producción, las lecturas parecen bien

Causa raíz: Latencia de fsync en la ruta de almacenamiento de producción (volumen de red, disco lento o contención del host). En dev, tu disco es rápido e inactivo.

Corrección: Mide el comportamiento de fsync; mueve los datos a SSD/NVMe local o a almacenamiento en bloque correctamente aprovisionado; evita overlay para datadir; ajusta scheduler de I/O y el host.

4) Síntoma: “Demasiadas conexiones” durante picos de tráfico

Causa raíz: Pool de conexiones mal configurado, contenedores autoescalando sin planificación de capacidad DB, o max_connections bajo respecto al workload.

Corrección: Arregla primero ajustes del pool (timeouts, max open connections). Luego dimensiona max_connections con contabilidad de memoria (buffers por conexión).

5) Síntoma: Consultas se comportan distinto entre MariaDB y MySQL

Causa raíz: Diferentes elecciones del optimizador, colaciones distintas o modos SQL diferentes.

Corrección: Pinea motor + versión entre entornos; ejecuta EXPLAIN en condiciones parecidas a prod; estandariza valores por defecto de esquema para charset/colación y modo SQL.

6) Síntoma: Contenedor “saludable” pero la app recibe timeouts justo después de reinicio

Causa raíz: La comprobación de salud solo verifica que el proceso esté en ejecución; la BD aún está en recuperación tras fallo o calentando caches.

Corrección: La comprobación de salud debería ejecutar una consulta (y opcionalmente verificar estado de replicación). Retrasa el arranque de la app hasta readiness, no hasta “contenedor iniciado”.

7) Síntoma: “Disco lleno” dentro del contenedor, el host tiene espacio libre

Causa raíz: Estás escribiendo en la capa del contenedor (overlay) en lugar del volumen; o el volumen está en otro filesystem que está lleno.

Corrección: Asegura que datadir apunte al volumen montado; verifica montajes; limpia binlogs y tmpdir con una política de retención.

8) Síntoma: Replicación se rompe tras un cambio de versión “menor”

Causa raíz: Expectativas de compatibilidad de binlog/GTID violadas; valores por defecto diferentes o comportamientos desaprobados entre versiones/motores.

Corrección: Actualiza con un plan escalonado; valida la replicación en un entorno preprod con patrones de tráfico reales; fija versiones hasta validar.

Tres mini-historias corporativas desde la trinchera

Mini-historia 1: El incidente causado por una suposición equivocada

El equipo tenía un Compose limpio. Un servicio MySQL, un servicio app y un volumen. Dev estaba contento. Staging “bien”. Producción se lanzó y murió en silencio: fallos intermitentes de login y un pico de 500 que parecía una regresión de la app.

La suposición fue sutil: “MySQL es MySQL”. En local habían estado corriendo MariaDB porque arrancaba más rápido y resultaba familiar. En producción, un ingeniero de plataforma lo cambió a mysql:8 para estandarizar. Sin mala intención; solo una limpieza bien intencionada.

La app usaba un conector MySQL antiguo embebido en una imagen base que no se había actualizado hace tiempo. Con carga ligera a veces funcionaba (gracias a reutilización de conexiones y una ruta de pruebas permisiva). Bajo carga real, nuevas conexiones aumentaron y la autenticación falló con errores de plugin.

La corrección no fue complicada: actualizar el conector y estandarizar las expectativas de autenticación. El daño fue el tiempo perdido porque todos depuraron “red” durante una hora. La lección: la selección de motor es parte del contrato de tu aplicación, no un detalle de infraestructura que puedas cambiar un viernes.

Mini-historia 2: La optimización que rebotó

Otra organización tenía un problema de rendimiento: las escrituras eran lentas. Alguien señaló a los sospechosos habituales—innodb_flush_log_at_trx_commit=1 y sync_binlog=1. Los cambiaron a 2 y 0 respectivamente. La latencia mejoró. El canal de incidentes se calmó. Choca esos cinco.

Dos semanas después, hubo un reinicio de host. Ni siquiera dramático. Tras el reinicio, la aplicación había perdido transacciones recientes. No muchas. Lo suficiente para ser doloroso, porque las filas faltantes eran visibles para clientes y sensibles en el tiempo.

Ahora el equipo tenía dos problemas: arreglar los datos y recuperar la confianza. Tuvieron que conciliar desde sistemas externos, reprocesar eventos y explicar por qué se había sacrificado durabilidad sin una decisión escrita. La causa raíz no fueron los ajustes per se; fue usarlos como parche de rendimiento en lugar de arreglar latencia de almacenamiento y contención de E/S.

La solución eventual fue aburrida: mover el volumen DB a almacenamiento en bloque correctamente aprovisionado, limitar tormentas de conexión y afinar consultas lentas. Los ajustes de durabilidad volvieron a estrictos, porque “rápido pero incorrecto” no es una característica.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Otro equipo ejecutaba MariaDB en contenedores para una plataforma interna. No eran glamorosos, pero sí disciplinados. Cada despliegue pineaba digests de imagen. Cada cambio de configuración pasaba por revisión. Los backups corrían cada noche con una prueba de restauración cada sprint.

Una mañana, tras un mantenimiento rutinario de nodo, la base de datos no arrancó. Los logs mostraban síntomas de corrupción en archivos de tablespace de InnoDB. Nadie celebró, pero nadie entró en pánico. Siguieron el runbook: detener escrituras, snapshot de lo que quedaba, restaurar el último backup conocido bueno, reproducir binlogs hasta el límite del incidente.

Volvieron al servicio con pérdida de datos limitada porque su retención y envío de binlogs estaban diseñados de antemano. La causa raíz fue un problema en la capa de almacenamiento de ese nodo, no MariaDB en sí. Pero la razón por la que no se convirtió en un evento de carrera fue simple: practicaban la recuperación como algo normal, no como un simulacro anual.

Un chiste corto (2/2): Los backups son como paracaídas—si esperas hasta necesitarlos para comprobarlos, ya tomaste una decisión.

Listas de verificación / plan paso a paso

Paso a paso: pasar de “éxito local” a “resiliencia en producción”

  1. Elige el motor y la versión deliberadamente. Documenta por qué usas MySQL vs MariaDB y fija la versión exacta.
  2. Estandariza la inyección de configuración. Prefiere un archivo de config montado o un mecanismo controlado; evita mezclar env vars y fragmentos de config sin una política.
  3. Define el almacenamiento. Decide: volumen nombrado vs bind mount; filesystem del host; clase de almacenamiento; requisitos de IOPS/durabilidad.
  4. Establece la durabilidad conscientemente. Mantén innodb_flush_log_at_trx_commit=1 y sync_binlog=1 a menos que hayas firmado tolerancia a pérdida de datos.
  5. Configura límites de memoria y ajusta para encajar. Limita max_connections, dimensiona buffer pool y verifica comportamiento OOM del contenedor.
  6. Implementa comprobaciones de readiness. Una health check que ejecute una consulta vence a “puerto abierto”.
  7. Registra y exporta métricas. Slow query log, error log y métricas MySQL/MariaDB básicas (conexiones, hit ratio del buffer pool, esperas de redo log).
  8. Backups con pruebas de restauración. Programa restores en un entorno desechable; valida compatibilidad de esquema y aplicación.
  9. Ensayo de upgrades. Ensaya actualizaciones de versión con datos y patrones de tráfico similares a producción. Mide tiempos de recuperación.
  10. Runbooks de incidentes. Ten un playbook para: latencia alta, lag de replicación, disco lleno y fallos de arranque.

Lista de despliegue (imprímela antes de enviar)

  • Etiqueta de imagen pineada y revisada; idealmente digest pineado.
  • Directorio de datos en un volumen (no en la capa del contenedor).
  • Sistema de archivos y opciones de montaje revisadas para idoneidad BD.
  • Límites de memoria/CPU establecidos; MySQL/MariaDB ajustado en consecuencia.
  • Compatibilidad plugin de auth/cliente validada.
  • Charset/colación estandarizados en el esquema, no dejarlos a valores por defecto.
  • Valores de durabilidad decididos y documentados explícitamente.
  • Backups configurados; restauración probada.
  • Monitorización y alertas para disco, CPU, memoria y latencia de consultas.
  • Comprobaciones de readiness/health validadas por consultas reales.

Preguntas frecuentes

1) ¿Puedo cambiar de MariaDB a MySQL solo cambiando la imagen Docker?

Puedes intentarlo, y a veces incluso arranca. Pero los formatos de directorio de datos, tablas del sistema y valores por defecto difieren entre versiones. Planea una migración: dump lógico/restore o corte basado en replicación.

2) ¿Por qué MySQL 8 rompe mi app cuando MariaDB funcionaba?

Lo más común: diferencias en plugin de autenticación, rigidez del modo SQL y valores por defecto de colación. Confirma con SELECT @@version_comment, plugins de usuario y @@sql_mode.

3) ¿Los volúmenes nombrados son más seguros que los bind mounts?

Operativamente, los volúmenes nombrados reducen trampas con permisos y escrituras accidentales en la capa del contenedor. Los bind mounts están bien cuando controlas el host y tienes prácticas fuertes sobre propiedad, labels y backups.

4) ¿Por qué la base de datos es más lenta en Docker que en bare metal?

Si el datadir está realmente en un volumen, la sobrecarga puede ser pequeña. Las grandes ralentizaciones suelen venir del almacenamiento subyacente (volúmenes en red, discos lentos, contención) y el comportamiento de fsync—no Docker en sí.

5) ¿Debo desactivar ajustes de durabilidad relacionados con fsync por rendimiento?

Sólo si puedes tolerar perder transacciones comprometidas tras un fallo. Para sistemas de producción que los usuarios confían, arregla la latencia de almacenamiento y los patrones de consulta primero. Cambiar la durabilidad es una decisión de negocio, no un tweak de rendimiento.

6) ¿Cuál es la forma más rápida de confirmar que el I/O es mi cuello de botella?

Busca en el estado de InnoDB esperas de log, correlación con latencia de commit y métricas de disco del host. En incidentes, el patrón “escrituras lentas, lecturas bien” es una pista clara.

7) ¿Por qué Compose “depends_on” no resuelve problemas de arranque?

Controla orden de inicio, no readiness. MySQL/MariaDB pueden aceptar conexiones antes de poder servir consultas de forma fiable (recuperación tras fallo, inicialización, migraciones).

8) ¿Puedo usar la misma estrategia de backups para contenedores MySQL y MariaDB?

Conceptualmente sí—dumps lógicos y respaldos físicos existen para ambos—pero las opciones de tooling y detalles de compatibilidad difieren. La regla real: prueba restores con el mismo motor/versión que correrás.

9) ¿Cuál es la forma más segura de ejecutar migraciones de esquema en contenedores?

Ejecuta migraciones como un job explícito con un mecanismo de lock/leader, y condiciona el despliegue de la app al éxito de la migración. No permitas que réplicas múltiples compitan por migrar al arrancar.

10) ¿Cómo prevengo actualizaciones accidentales al reconstruir imágenes?

Pinea imágenes base, pinea etiquetas/digests de la imagen DB y trata cambios de versión como eventos gestionados con plan de rollback.

Conclusión: siguientes pasos para no reaprender esto

“Funcionó localmente” suele ser cierto. Simplemente irrelevante. Producción es otra máquina con modos de fallo diferentes, y las bases de datos magnifican cada pequeña mentira que tu entorno te cuenta.

Haz esto a continuación, en orden:

  1. Elige el motor y fija la versión (y deja de fingir que MySQL y MariaDB son intercambiables en tiempo de ejecución).
  2. Audita tu ruta de almacenamiento para el datadir: tipo de sistema de archivos, opciones de montaje y latencia real de fsync bajo carga.
  3. Estandariza valores por defecto: sql_mode, charset/colación, expectativas de autenticación—hazlos explícitos.
  4. Implementa comprobaciones de readiness y simulacros de recuperación: una base de datos que no puedes restaurar es un rumor, no un sistema.
  5. Ejecuta las tareas prácticas arriba y guarda sus resultados en tu runbook para poder comparar antes/después de cambios.

Si no haces otra cosa: deja de usar “latest”, deja de tratar el directorio de datos como una carpeta casual y deja que los valores por defecto decidan la corrección. Producción recompensa la rutina. Gánatela.

← Anterior
Construye una tabla de precios SaaS que convierta sin romper tu frontend
Siguiente →
Docker «red no encontrada»: reconstruir redes sin romperlo todo

Deja un comentario