Por qué autoalojar MySQL en un VPS
En un alojamiento compartido, su base MySQL comparte la RAM y la I/O con cientos de otros sitios, y usted no puede modificar ni innodb_buffer_pool_size, ni los modos SQL, ni la versión del motor. En un VPS dispone de recursos dedicados, elige entre MySQL 8.4 LTS y MariaDB, y accede al archivo my.cnf para optimizar cada parámetro. Es la opción adecuada para una tienda de comercio electrónico, un CMS con mucho tráfico o una API que necesita transacciones rápidas y un buffer pool generoso. También gestiona sus propios usuarios, sus grants a nivel de tabla y su estrategia de replicación maestro-esclavo.
Los beneficios concretos de un MySQL autoalojado
- Recursos dedicados: todo el buffer pool de InnoDB para sus datos, sin vecinos ruidosos.
- Acceso completo a
my.cnf:innodb_buffer_pool_size,max_connections, modos SQL estrictos. - Elección del motor: MySQL 8.4 LTS para la estabilidad a largo plazo, o MariaDB por sus motores adicionales.
- Replicación maestro-esclavo configurable para derivar las lecturas o para la recuperación ante incidentes.
- Copias de seguridad lógicas (
mysqldump) o físicas (xtrabackup) según su volumen. - Sin cuota arbitraria sobre el tamaño de las bases de datos ni sobre el número de conexiones simultáneas.
Requisitos concretos antes de empezar
Versión recomendada: MySQL 8.4 LTS (versión actual: 8.4.11), la rama LTS oficial, con soporte hasta 2032. Evite MySQL 8.0 en instalaciones nuevas: el soporte activo termina en 2025.
RAM: 1 GB como mínimo para un entorno de prueba o un blog ligero. Planifique 2 GB para un WordPress o una pequeña aplicación en producción, y 4 a 8 GB para Magento, PrestaShop o una API de alto tráfico. La regla base: asigne el 60-70% de la RAM disponible a innodb_buffer_pool_size.
CPU: 2 vCPU cubren la mayoría de los casos. Escale a 4 vCPU cuando las consultas sean frecuentes o active la replicación.
Almacenamiento: 30 GB de SSD NVMe como punto de partida; reserve espacio adicional para los binlogs si activa la replicación o la restauración point-in-time.
Puertos: MySQL escucha por defecto en el puerto 3306. Este puerto nunca debe estar abierto en la interfaz pública. El parámetro bind-address en my.cnf debe apuntar a 127.0.0.1 o a la dirección de red privada, no a 0.0.0.0.
Sistema: Ubuntu 22.04 o 24.04 LTS, Docker y Docker Compose v2, un volumen persistente dedicado.
Desplegar MySQL 8.4 con Docker en producción
Asegurar el VPS
Actualice el sistema (
apt update && apt upgrade -y), instale Docker, desactive la conexión SSH por contraseña en favor de las claves (PasswordAuthentication noensshd_config) y cierre el puerto 3306 hacia el exterior:ufw deny 3306 ufw allow OpenSSH ufw enableMySQL solo debe escuchar en la red interna del contenedor Docker.
Definir el docker-compose.yml
Declare un servicio
mysql:8.4, monte un volumen en/var/lib/mysqly pase las variables de entorno mediante un archivo.env:# .env MYSQL_ROOT_PASSWORD=contraseña_root_fuerte MYSQL_DATABASE=mi_base MYSQL_USER=app_user MYSQL_PASSWORD=contraseña_app# docker-compose.yml services: mysql: image: mysql:8.4 restart: unless-stopped env_file: .env volumes: - mysql_data:/var/lib/mysql - ./conf.d:/etc/mysql/conf.d:ro ports: - "127.0.0.1:3306:3306" volumes: mysql_data:Nótese el bind en
127.0.0.1enports: el puerto solo será accesible desde el propio VPS.Crear una base de datos y un usuario dedicado
Tras
docker compose up -d, conéctese al contenedor:docker exec -it mysql mysql -u root -pCree la base de datos y el usuario de aplicación:
CREATE DATABASE mi_base CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'app_user'@'%' IDENTIFIED BY 'contraseña_app'; GRANT SELECT, INSERT, UPDATE, DELETE ON mi_base.* TO 'app_user'@'%'; FLUSH PRIVILEGES;Limite los grants al mínimo necesario: nunca use
GRANT ALL PRIVILEGESpara una cuenta de aplicación.Endurecer la instalación
Ejecute el equivalente de
mysql_secure_installationmanualmente:-- Eliminar usuarios anónimos DELETE FROM mysql.user WHERE User=''; -- Prohibir el inicio de sesión root remoto DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1'); -- Eliminar la base de datos de prueba DROP DATABASE IF EXISTS test; FLUSH PRIVILEGES;No utilice nunca la cuenta
rootdesde su aplicación. Verifique quebind-address = 127.0.0.1esté presente en suconf.d/custom.cnf.Exponer en TLS si es necesario
Si una aplicación externa debe conectarse de forma remota, nunca exponga el puerto 3306 directamente en Internet. Utilice un túnel SSH:
ssh -L 3306:127.0.0.1:3306 [email protected] -NConecte su cliente MySQL a
127.0.0.1:3306— el tráfico viaja cifrado por SSH. Para conexiones permanentes entre VPS, configure MySQL para escuchar solo en la interfaz de red privada y permita solo la IP del servidor de aplicación en UFW.Planificar las copias de seguridad
Programe un
mysqldump --single-transactiondiario mediante cron para obtener dumps coherentes sin bloqueo:# /etc/cron.d/mysql-backup 0 2 * * * root docker exec mysql mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" --single-transaction --all-databases | gzip > /backups/mysql-$(date +\%Y\%m\%d).sql.gzConserve al menos 7 días de retención y archive los dumps fuera del VPS. Pruebe la restauración una vez al mes: un backup no probado no es un backup. Para bases de datos grandes, pase a Percona XtraBackup.
Seguridad: los gestos imprescindibles
mysql_secure_installation en un contenedor. La imagen Docker oficial no lo ejecuta automáticamente: hágalo manualmente tras el primer arranque (paso 4 anterior).
Eliminación del root remoto. La cuenta root solo debe ser accesible desde localhost. Una cuenta root accesible desde % es la primera vulnerabilidad explotada durante un escaneo de puertos.
Usuario dedicado por aplicación. Cada aplicación conectada recibe su propia cuenta MySQL con derechos restringidos a su propia base de datos. Una aplicación comprometida no podrá leer ni modificar los datos de las demás.
UFW + bind-address. La doble protección es obligatoria: ufw deny 3306 a nivel del firewall del sistema, y bind-address = 127.0.0.1 en my.cnf del lado de MySQL. Uno no sustituye al otro.
Contraseñas fuertes. Use caching_sha2_password (plugin por defecto en MySQL 8.x) y contraseñas generadas (mínimo 20 caracteres). Guárdelas en secretos gestionados, nunca en el código fuente.
Optimizar el rendimiento de InnoDB
Estos parámetros se añaden en su archivo conf.d/custom.cnf montado en solo lectura en el contenedor.
innodb_buffer_pool_size: el parámetro más importante. Configúrelo al 60-70% de la RAM del VPS. En un VPS de 4 GB, eso supone entre 2,5 y 2,8 GB. Demasiado pequeño = lecturas de disco frecuentes; demasiado grande = swap.
innodb_flush_log_at_trx_commit: el valor 2 ofrece un buen equilibrio rendimiento/durabilidad para la mayoría de aplicaciones. El valor 1 (por defecto) es más seguro pero más lento; el 0 es el más rápido pero arriesgado en caso de fallo.
max_connections: ajústelo según su carga real. El valor por defecto (151) suele ser demasiado bajo para una aplicación de alto tráfico. Cada conexión consume ~1 MB de RAM.
slow_query_log: actívelo con slow_query_log = 1 y long_query_time = 1 para capturar todas las consultas que superen 1 segundo. Es su primera herramienta de optimización.
Ejemplo de custom.cnf para un VPS de 4 GB:
[mysqld]
innodb_buffer_pool_size = 2G
innodb_flush_log_at_trx_commit = 2
max_connections = 200
slow_query_log = 1
long_query_time = 1
log_bin = /var/lib/mysql/binlog
binlog_format = ROWAcceso remoto seguro: el túnel SSH
Exponer el puerto 3306 en la interfaz pública de su VPS es un grave error de seguridad: los escáneres automáticos intentan ataques de fuerza bruta sobre este puerto continuamente. La buena práctica es el túnel SSH.
Conexión puntual desde su equipo:
ssh -L 3306:127.0.0.1:3306 [email protected] -NAbra su cliente MySQL sobre 127.0.0.1:3306 — el tráfico viaja cifrado por SSH.
Conexión permanente entre VPS: si su aplicación corre en un segundo VPS de la misma red privada, configure MySQL para escuchar solo en la dirección de red privada (bind-address = 10.0.0.X) y permita solo la IP del servidor de aplicación en UFW:
ufw allow from 10.0.0.5 to any port 3306A evitar: bind-address = 0.0.0.0 sin firewall, puerto 3306 abierto públicamente, o cuentas MySQL con Host='%' sin restricción de IP.
Copias de seguridad automatizadas: estrategia completa
Una estrategia de copia de seguridad MySQL se apoya en dos niveles complementarios.
Nivel 1 — dump lógico diario (mysqldump): consistente gracias a --single-transaction (sin bloqueo de lectura en InnoDB), portable entre versiones. Adecuado para bases de hasta unas decenas de GB.
Nivel 2 — binlogs para restauración point-in-time: active log_bin desde el principio. Combinados con un dump completo semanal, los binlogs permiten reproducir cada transacción hasta el segundo anterior a un incidente.
Para bases grandes — Percona XtraBackup: copia de seguridad física sin interrumpir el servicio, restauración rápida (copia de archivos en lugar de re-ejecución SQL).
Retención y archivado: no almacene las copias de seguridad únicamente en el VPS. Un VPS corrupto se lleva sus propios backups. Exporte a almacenamiento de objetos o a un servidor SFTP remoto, y conserve al menos 7 días.
Probar la restauración: un backup no probado no es un backup. Planifique una prueba mensual: restaure en un contenedor temporal y verifique que sus datos están intactos.
# Restauración desde un dump comprimido
zcat /backups/mysql-20260917.sql.gz | docker exec -i mysql mysql -u root -pResolución de errores: 5 errores frecuentes
Access denied for user 'app'@'...'
La cuenta existe pero el Host no coincide. MySQL verifica la IP de origen exacta. Si la aplicación se conecta desde un contenedor Docker, la IP de origen puede ser 172.17.0.x y no localhost. Compruebe con:
SELECT User, Host FROM mysql.user;Solución: cree la cuenta con 'app_user'@'%' (o la IP exacta del contenedor) y ejecute FLUSH PRIVILEGES.
Too many connections
Se ha alcanzado max_connections. Compruebe el número de conexiones activas:
SHOW STATUS LIKE 'Threads_connected';Aumente max_connections en custom.cnf y verifique que su aplicación usa un pool de conexiones.
Table '...' is marked as crashed
Corrupción de InnoDB, a menudo tras un cierre brusco. Intente la reparación:
mysqlcheck -u root -p --auto-repair --all-databasesSi la tabla no puede repararse, restaure desde su último backup consistente.
Can't connect to MySQL server on '127.0.0.1' (111)
MySQL no está escuchando en la interfaz esperada. Compruebe bind-address en custom.cnf y reinicie el contenedor. Verifique también que el contenedor está en marcha con docker compose ps.
ERROR 1819 (HY000): Your password does not satisfy the current policy
El plugin de validación de contraseñas de MySQL 8 rechaza las contraseñas simples. Use una contraseña de al menos 8 caracteres con mayúsculas, dígitos y símbolos, o establezca validate_password.policy = LOW en custom.cnf solo para entornos de desarrollo.
Active los binlogs (log_bin + binlog_format=ROW) desde el principio: son la clave de la restauración point-in-time. Combinados con un mysqldump completo semanal, permiten reproducir todas las transacciones hasta el segundo anterior a un incidente. Y si monta un esclavo en replicación en un segundo VPS, esos mismos binlogs alimentan la réplica en tiempo real para sus lecturas derivadas.