--- title: "Migrar una web con MySQL a otro servidor sin apenas cortes" description: "Migra una web con Nginx y MySQL a un servidor nuevo con rsync y replicación GTID, pruébala antes del cambio y cambia el DNS con un corte de pocos minutos." category: "Migración y Recuperación ante Desastres" language: es source: https://cubepath.com/es/docs/migración-y-recuperación-ante-desastres/migracion-en-vivo-de-servicios-entre-servidores updated: 2026-09-25 --- # Migrar una web con MySQL a otro servidor sin apenas cortes Mover una aplicación a un servidor nuevo no tiene por qué implicar horas de parada. Si copias los datos por adelantado, mantienes la base de datos replicada hasta el último momento y preparas el DNS con antelación, el corte real se reduce a unos minutos. En esta guía migrarás una web con Nginx, PHP y MySQL de un servidor Ubuntu 24.04 a otro usando rsync, replicación de MySQL con GTID y un cambio de DNS planificado, con un plan de marcha atrás por si algo falla. ## Requisitos previos - Un servidor de **origen** (`source_ip`) con la web en producción: Nginx, PHP-FPM y MySQL 8.0 en Ubuntu 24.04. Los datos de la web están en `/var/www/your_domain`. - Un servidor de **destino** (`dest_ip`) con Ubuntu 24.04 LTS recién instalado, por ejemplo un VPS de CubePath, con los mismos paquetes y versiones: `sudo apt install nginx mysql-server php-fpm php-mysql` y las extensiones de PHP que use tu aplicación. - Un usuario no root con privilegios `sudo` (`your_user`) en los dos servidores. - Acceso al panel de tu proveedor de DNS para el dominio `your_domain`. - Una copia de seguridad reciente del origen, independiente de esta migración. ## Paso 1: Bajar el TTL del DNS con antelación El TTL indica cuánto tiempo pueden guardar en caché los resolutores la respuesta DNS. Si el registro `A` de tu dominio tiene un TTL de 1 hora, tras el cambio algunos clientes seguirán yendo al servidor antiguo hasta una hora después. Al menos 24 horas antes de la migración (o el valor del TTL actual, si es mayor), baja el TTL de los registros `A` y `AAAA` de `your_domain` y `www.your_domain` a 300 segundos en el panel de tu proveedor de DNS. Comprueba el valor que se está sirviendo: ```bash dig +noall +answer your_domain A ``` ```output your_domain. 300 IN A source_ip ``` La segunda columna es el TTL. Deja pasar el tiempo del TTL antiguo antes de continuar con el paso 7. ## Paso 2: Preparar el acceso SSH entre servidores Los archivos se copiarán desde el destino tirando del origen. Para conservar propietarios y permisos, rsync debe ejecutarse como root en ambos extremos. En lugar de permitir el acceso SSH como root, darás a tu usuario del origen permiso para ejecutar solo `rsync` con `sudo` sin contraseña. En el **origen**, crea la regla de sudo con `visudo`, que valida la sintaxis antes de guardar: ```bash sudo visudo -f /etc/sudoers.d/migration ``` ```text your_user ALL=(root) NOPASSWD: /usr/bin/rsync ``` En el **destino**, genera una clave SSH para root y muestra la parte pública: ```bash sudo ssh-keygen -t ed25519 -f /root/.ssh/migration -N "" -C "migration" sudo cat /root/.ssh/migration.pub ``` Añade esa línea a `~/.ssh/authorized_keys` de `your_user` en el origen. Comprueba desde el destino que funciona: ```bash sudo ssh -i /root/.ssh/migration your_user@source_ip 'sudo rsync --version | head -1' ``` ```output rsync version 3.2.7 protocol version 31 ``` ## Paso 3: Hacer la copia inicial de los archivos La primera sincronización transfiere todos los datos y puede tardar. Como la web sigue en marcha, la copia no será exacta, pero no importa: las siguientes ejecuciones solo transferirán las diferencias. En el **destino**, copia la web, la configuración de Nginx y los certificados TLS: ```bash sudo rsync -aHAX --numeric-ids --info=progress2 \ -e "ssh -i /root/.ssh/migration" --rsync-path="sudo rsync" \ your_user@source_ip:/var/www/your_domain/ /var/www/your_domain/ ``` ```bash sudo rsync -aHAX --numeric-ids \ -e "ssh -i /root/.ssh/migration" --rsync-path="sudo rsync" \ your_user@source_ip:/etc/nginx/sites-available/ /etc/nginx/sites-available/ ``` ```bash sudo rsync -aHAX --numeric-ids \ -e "ssh -i /root/.ssh/migration" --rsync-path="sudo rsync" \ your_user@source_ip:/etc/letsencrypt/ /etc/letsencrypt/ ``` Las opciones `-aHAX --numeric-ids` conservan permisos, enlaces duros, ACL, atributos extendidos y los identificadores numéricos de usuario y grupo. La barra final en las rutas de origen copia el contenido del directorio y no el directorio en sí. Si tu aplicación usa otras rutas (configuración de PHP-FPM en `/etc/php/8.3/fpm/pool.d/`, tareas cron, archivos subidos fuera de `/var/www`), sincronízalas igual. Activa el sitio en el destino y comprueba la configuración de Nginx: ```bash sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/ sudo nginx -t ``` ```output nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ``` Si copias `/etc/letsencrypt`, instala también certbot en el destino (`sudo apt install certbot python3-certbot-nginx`) para que las renovaciones sigan funcionando después del cambio. ## Paso 4: Replicar la base de datos al destino Volcar e importar la base de datos durante el corte obliga a parar la web mientras dura la importación. Con replicación, el destino se mantiene al día con cada escritura y el corte solo espera a que se apliquen los últimos segundos de cambios. En el **origen**, activa el registro binario con GTID y haz que MySQL escuche en su IP: ```bash sudo nano /etc/mysql/mysql.conf.d/replication.cnf ``` ```ini [mysqld] server-id = 1 bind-address = source_ip log_bin = /var/log/mysql/mysql-bin.log gtid_mode = ON enforce_gtid_consistency = ON ``` > Nota: si MySQL ya estaba en marcha con `gtid_mode = OFF` y tiene tráfico, activar GTID de golpe es seguro en un servidor independiente como este, pero requiere reiniciar MySQL. Hazlo en un momento de poco tráfico: es un reinicio de unos segundos. Reinicia MySQL, abre el puerto solo para el destino y crea el usuario de replicación: ```bash sudo systemctl restart mysql sudo ufw allow from dest_ip to any port 3306 proto tcp sudo mysql -e "CREATE USER 'repl'@'dest_ip' IDENTIFIED BY 'your_strong_password' REQUIRE SSL; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'dest_ip';" ``` En el **destino**, crea el mismo archivo con `server-id = 2` y sin `bind-address`, y reinicia MySQL: ```bash sudo nano /etc/mysql/mysql.conf.d/replication.cnf ``` ```ini [mysqld] server-id = 2 log_bin = /var/log/mysql/mysql-bin.log gtid_mode = ON enforce_gtid_consistency = ON ``` ```bash sudo systemctl restart mysql ``` En el **origen**, genera un volcado coherente de la base de datos de la aplicación, incluidos los GTID ya aplicados, y cópialo al destino: ```bash sudo mysqldump --single-transaction --routines --triggers --events \ --set-gtid-purged=ON --databases your_database > ~/initial.sql rsync -avz ~/initial.sql your_user@dest_ip:~/ ``` En el **destino**, importa el volcado y arranca la replicación: ```bash sudo mysql -e "RESET MASTER;" sudo mysql < ~/initial.sql ``` ```bash sudo mysql ``` ```sql CHANGE REPLICATION SOURCE TO SOURCE_HOST = 'source_ip', SOURCE_USER = 'repl', SOURCE_PASSWORD = 'your_strong_password', SOURCE_AUTO_POSITION = 1, SOURCE_SSL = 1; START REPLICA; SET PERSIST super_read_only = ON; EXIT; ``` `super_read_only` evita que las pruebas del paso 5 escriban en la base de datos del destino y rompan la replicación. Comprueba el estado: ```bash sudo mysql -e "SHOW REPLICA STATUS\G" | grep -E "Replica_IO_Running|Replica_SQL_Running:|Seconds_Behind_Source" ``` ```output Replica_IO_Running: Yes Replica_SQL_Running: Yes Seconds_Behind_Source: 0 ``` El usuario de MySQL de la aplicación no está en el volcado, porque solo se ha copiado `your_database`. Créalo en el **destino** con la misma contraseña que figura en la configuración de la aplicación, desactivando `super_read_only` solo durante la operación: ```bash sudo mysql ``` ```sql SET GLOBAL super_read_only = OFF; CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'app_user_password'; GRANT ALL PRIVILEGES ON your_database.* TO 'app_user'@'localhost'; SET GLOBAL super_read_only = ON; EXIT; ``` Ajusta los privilegios a los que tenga el usuario en el origen; puedes consultarlos allí con `SHOW GRANTS FOR 'app_user'@'localhost';`. ## Paso 5: Probar el destino antes del cambio Prueba la web en el destino sin tocar el DNS. `curl --resolve` fuerza la resolución del dominio a la IP que indiques: ```bash curl -sI --resolve your_domain:443:dest_ip https://your_domain/ ``` ```output HTTP/2 200 server: nginx/1.24.0 (Ubuntu) content-type: text/html; charset=UTF-8 ``` Para probar con el navegador, añade temporalmente la línea `dest_ip your_domain www.your_domain` al archivo `hosts` de tu equipo, navega por la web y revisa los registros del destino: ```bash sudo tail -f /var/log/nginx/error.log ``` Mientras la base de datos del destino esté en solo lectura, las acciones que escriben (iniciar sesión, publicar) darán error: es lo esperado. Lo importante es que las páginas carguen, PHP funcione y la aplicación conecte con MySQL. Quita la línea del archivo `hosts` al terminar. ## Paso 6: Hacer el corte Este es el único momento con parada. Prepara todos los comandos antes de empezar. Repite primero la sincronización de archivos (paso 3) para que la diferencia pendiente sea mínima. En el **origen**, detén la web para que no entren más escrituras: ```bash sudo systemctl stop nginx ``` Comprueba que el destino ha aplicado todas las transacciones del origen. Consulta el conjunto de GTID ejecutados en el **origen**: ```bash sudo mysql -e "SELECT @@GLOBAL.gtid_executed;" ``` Ejecuta la misma consulta en el **destino** y compara: los dos valores deben coincidir. Si no, espera unos segundos y vuelve a consultar. En el **destino**, haz la última sincronización de archivos, ahora con `--delete` para eliminar lo que se haya borrado en el origen: ```bash sudo rsync -aHAX --numeric-ids --delete \ -e "ssh -i /root/.ssh/migration" --rsync-path="sudo rsync" \ your_user@source_ip:/var/www/your_domain/ /var/www/your_domain/ ``` Promueve la base de datos del destino a independiente y permite escrituras: ```bash sudo mysql ``` ```sql STOP REPLICA; RESET REPLICA ALL; SET PERSIST super_read_only = OFF; SET PERSIST read_only = OFF; EXIT; ``` Arranca o recarga Nginx en el destino: ```bash sudo systemctl reload nginx ``` Por último, cambia en tu proveedor de DNS los registros `A` (y `AAAA`, si tienes IPv6) de `your_domain` y `www.your_domain` a `dest_ip`. ## Paso 7: Redirigir el tráfico que siga llegando al origen Durante unos minutos, los clientes con la respuesta DNS antigua en caché seguirán llegando al origen. En lugar de mostrarles un error, el origen puede reenviar esas peticiones al destino con Nginx como proxy inverso. En el **origen**, desactiva el sitio original y crea uno de reenvío: ```bash sudo rm /etc/nginx/sites-enabled/your_domain sudo nano /etc/nginx/sites-available/migration-proxy ``` ```nginx server { listen 80; listen 443 ssl; server_name your_domain www.your_domain; ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem; location / { proxy_pass https://dest_ip; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_ssl_server_name on; proxy_ssl_name $host; } } ``` Actívalo y arranca Nginx: ```bash sudo ln -s /etc/nginx/sites-available/migration-proxy /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl start nginx ``` Comprueba que el origen devuelve ya el contenido del destino: ```bash curl -sI --resolve your_domain:443:source_ip https://your_domain/ ``` Si tu aplicación registra la IP del cliente, configura en el destino el módulo `real_ip` de Nginx para confiar en la cabecera `X-Forwarded-For` que envía `source_ip`. Mantén este reenvío al menos 24 horas. ## Paso 8: Verificar la migración Comprueba que el DNS público ya devuelve la nueva IP: ```bash dig +short your_domain A @1.1.1.1 dig +short your_domain A @8.8.8.8 ``` ```output dest_ip ``` En el **destino**, confirma que llegan peticiones y que no hay errores: ```bash sudo tail -n 20 /var/log/nginx/access.log sudo tail -n 20 /var/log/nginx/error.log ``` Comprueba que las escrituras funcionan (inicia sesión, crea un contenido de prueba) y que la renovación de certificados se puede ejecutar: ```bash sudo certbot renew --dry-run ``` Cuando todo sea correcto, vuelve a subir el TTL del DNS a su valor habitual. ## Marcha atrás Si detectas un problema grave poco después del corte, puedes volver al origen mientras no haya escrituras importantes en el destino: 1. En el origen, borra `/etc/nginx/sites-enabled/migration-proxy`, vuelve a enlazar el sitio original y recarga Nginx. 2. Devuelve los registros DNS a `source_ip`. 3. Detén Nginx en el destino. Si el destino ya ha recibido escrituras, esos datos no están en el origen. Antes de volver, vuelca las tablas afectadas en el destino e impórtalas en el origen, o decide conscientemente descartarlas. Por eso conviene tener definido de antemano un plazo (por ejemplo, la primera hora) durante el cual la marcha atrás es la opción por defecto ante un fallo. ## Limpieza Cuando la migración esté consolidada y el tráfico al origen haya cesado: - Borra `/etc/sudoers.d/migration` y la clave de migración de `authorized_keys` en el origen. - Elimina el usuario `repl` y la regla de UFW del puerto 3306 en el origen. - Borra `/etc/mysql/mysql.conf.d/replication.cnf` en el destino si no vas a usar replicación, o mantenlo para colgar una réplica nueva. - Haz una última copia de seguridad del origen antes de darlo de baja. ## Solución de problemas - **`sudo: a terminal is required to read the password`** al ejecutar rsync: la regla de sudoers no coincide. Comprueba que la ruta es `/usr/bin/rsync` (`command -v rsync`) y que el usuario es el correcto. - **`Replica_IO_Running: Connecting`**: el destino no llega al puerto 3306 del origen. Revisa `bind-address`, la regla de UFW y prueba con `nc -zv source_ip 3306`. - **502 Bad Gateway en el origen tras el paso 7**: el origen no puede conectar con el destino por HTTPS. Prueba desde el origen `curl -sI --resolve your_domain:443:dest_ip https://your_domain/`. - **La aplicación no conecta con MySQL en el destino**: el usuario de la aplicación no existe o tiene otra contraseña. Compáralo con `SELECT user, host FROM mysql.user;` en ambos servidores. ## Conclusión Has migrado una web con su base de datos a un servidor nuevo con un corte de pocos minutos: copia previa con rsync, replicación de MySQL hasta el último momento, pruebas sin tocar el DNS y un reenvío temporal desde el servidor antiguo para no perder tráfico durante la propagación. El mismo procedimiento sirve para mover servicios entre centros de datos. Como siguientes pasos, configura copias de seguridad automáticas en el nuevo servidor, documenta la migración en tu plan de recuperación ante desastres y, si quieres poder conmutar en caliente en el futuro, mantén una réplica permanente en otra ubicación.