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-mysqly 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:
dig +noall +answer your_domain A
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:
sudo visudo -f /etc/sudoers.d/migration
your_user ALL=(root) NOPASSWD: /usr/bin/rsync
En el destino, genera una clave SSH para root y muestra la parte pública:
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:
sudo ssh -i /root/.ssh/migration your_user@source_ip 'sudo rsync --version | head -1'
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:
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/
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/
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:
sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
sudo nginx -t
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:
sudo nano /etc/mysql/mysql.conf.d/replication.cnf
[mysqld]
server-id = 1
bind-address = source_ip
log_bin = /var/log/mysql/mysql-bin.log
gtid_mode = ON
enforce_gtid_consistency = ON
Notasi MySQL ya estaba en marcha con
gtid_mode = OFFy 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:
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:
sudo nano /etc/mysql/mysql.conf.d/replication.cnf
[mysqld]
server-id = 2
log_bin = /var/log/mysql/mysql-bin.log
gtid_mode = ON
enforce_gtid_consistency = ON
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:
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:
sudo mysql -e "RESET MASTER;"
sudo mysql < ~/initial.sql
sudo mysql
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:
sudo mysql -e "SHOW REPLICA STATUS\G" | grep -E "Replica_IO_Running|Replica_SQL_Running:|Seconds_Behind_Source"
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:
sudo mysql
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:
curl -sI --resolve your_domain:443:dest_ip https://your_domain/
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:
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:
sudo systemctl stop nginx
Comprueba que el destino ha aplicado todas las transacciones del origen. Consulta el conjunto de GTID ejecutados en el origen:
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:
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:
sudo mysql
STOP REPLICA;
RESET REPLICA ALL;
SET PERSIST super_read_only = OFF;
SET PERSIST read_only = OFF;
EXIT;
Arranca o recarga Nginx en el destino:
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:
sudo rm /etc/nginx/sites-enabled/your_domain
sudo nano /etc/nginx/sites-available/migration-proxy
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:
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:
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:
dig +short your_domain A @1.1.1.1
dig +short your_domain A @8.8.8.8
dest_ip
En el destino, confirma que llegan peticiones y que no hay errores:
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:
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:
- En el origen, borra
/etc/nginx/sites-enabled/migration-proxy, vuelve a enlazar el sitio original y recarga Nginx. - Devuelve los registros DNS a
source_ip. - 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/migrationy la clave de migración deauthorized_keysen el origen. - Elimina el usuario
reply la regla de UFW del puerto 3306 en el origen. - Borra
/etc/mysql/mysql.conf.d/replication.cnfen 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 passwordal 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. Revisabind-address, la regla de UFW y prueba connc -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.
