Si copias los datos una sola vez, todo lo que cambie en el origen después de esa copia se pierde o te obliga a una ventana de corte larga. La solución es mantener el servidor nuevo sincronizado con el antiguo mientras este sigue en producción, de modo que el corte se reduzca a detener las escrituras, esperar unos segundos y cambiar el DNS. En este tutorial sincronizarás de forma continua un directorio de archivos con lsyncd y una base de datos con replicación nativa de MySQL o PostgreSQL entre dos servidores con Ubuntu 24.04, y harás el cambio final de forma ordenada.
Requisitos previos
- Dos servidores con Ubuntu 24.04 LTS: el origen, en producción, y el destino, por ejemplo un VPS de CubePath, con conectividad entre ellos por IP.
- Un usuario no root con privilegios
sudoen ambos. - La misma versión mayor de la base de datos en los dos servidores: MySQL 8.0 o PostgreSQL 16, que son las que incluye Ubuntu 24.04. La replicación física de PostgreSQL no funciona entre versiones mayores distintas.
- Una primera copia completa de los archivos ya hecha con
rsync, o tiempo para hacerla.
En esta guía, old_server_ip es la IP del origen, new_server_ip la del destino, /var/www/ el directorio de archivos y app_db la base de datos. Sustitúyelos por tus valores.
Qué método usar para cada dato
| Tipo de dato | Método | Retraso típico |
|---|---|---|
| Archivos que cambian poco (código, configuración) | rsync repetido a mano | El que tú decidas |
| Archivos que cambian continuamente (subidas de usuarios) | lsyncd: inotify + rsync | Unos segundos |
| MySQL o MariaDB | Replicación nativa con GTID | Menos de un segundo |
| PostgreSQL | Replicación por streaming | Menos de un segundo |
No copies los archivos de datos de una base de datos en funcionamiento (/var/lib/mysql, /var/lib/postgresql) con rsync o lsyncd: la copia sería inconsistente. Las bases de datos se sincronizan siempre con sus propios mecanismos.
Paso 1: Sincronizar archivos de forma continua con lsyncd
lsyncd vigila un directorio con inotify, agrupa los cambios durante unos segundos y lanza rsync solo sobre los archivos modificados. Se ejecuta en el origen como servicio de systemd y, como root, necesita acceso SSH como root al destino.
En el origen, crea una clave SSH para root:
sudo ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ""
sudo cat /root/.ssh/id_ed25519.pub
En el destino, añade esa clave pública a las claves autorizadas de root:
sudo mkdir -p /root/.ssh
sudo nano /root/.ssh/authorized_keys
Pega la línea de la clave y guarda. Ubuntu permite por defecto el acceso de root solo con clave (PermitRootLogin prohibit-password). Comprueba desde el origen que la conexión funciona y acepta la huella del destino:
sudo ssh root@new_server_ip hostname
Instala lsyncd en el origen y crea el directorio de logs:
sudo apt update
sudo apt install lsyncd
sudo mkdir -p /etc/lsyncd /var/log/lsyncd
Crea el archivo de configuración. El paquete de Ubuntu lo lee de /etc/lsyncd/lsyncd.conf.lua:
sudo nano /etc/lsyncd/lsyncd.conf.lua
settings {
logfile = "/var/log/lsyncd/lsyncd.log",
statusFile = "/var/log/lsyncd/lsyncd.status",
insist = true,
}
sync {
default.rsyncssh,
source = "/var/www/",
host = "root@new_server_ip",
targetdir = "/var/www/",
delay = 5,
exclude = { "cache/", "*.tmp" },
rsync = {
archive = true,
compress = false,
_extra = { "--numeric-ids" },
},
}
default.rsyncsshusa rsync para transferir archivos y SSH para mover y borrar en el destino, lo que evita volver a copiar un archivo solo porque se ha renombrado.delay = 5agrupa los cambios de cinco segundos en una sola ejecución de rsync.insist = truehace que lsyncd arranque aunque el destino no responda y reintente después.- Por defecto lsyncd borra en el destino lo que se borra en el origen, igual que
rsync --delete.
Al arrancar, lsyncd hace una sincronización completa del directorio y después solo envía los cambios. Habilita e inicia el servicio:
sudo systemctl enable --now lsyncd
sudo systemctl status lsyncd --no-pager
Sigue el log mientras se hace la sincronización inicial. Cuando termine aparecerá una línea con Startup of "/var/www/" finished y no debe haber líneas con Error:
sudo tail -f /var/log/lsyncd/lsyncd.log
Para probarlo, crea un archivo en el origen y comprueba que aparece en el destino unos segundos después:
sudo touch /var/www/prueba-lsyncd.txt
sleep 10
sudo ssh root@new_server_ip ls -l /var/www/prueba-lsyncd.txt
Borra el archivo de prueba en el origen; también desaparecerá del destino.
Notacada directorio vigilado consume un "watch" de inotify. En directorios con cientos de miles de subdirectorios, comprueba el límite con
cat /proc/sys/fs/inotify/max_user_watches. Si el log muestra errores de inotify, súbelo creando/etc/sysctl.d/90-inotify.confconfs.inotify.max_user_watches = 1048576y aplícalo consudo sysctl --system.
Paso 2: Replicar una base de datos MySQL
La replicación nativa hace que el destino aplique en tiempo real cada cambio del origen. Con GTID, cada transacción tiene un identificador único y el destino sabe exactamente por dónde continuar.
Configurar el origen
En el origen, crea un archivo de configuración para la replicación. Los archivos de /etc/mysql/mysql.conf.d/ se leen en orden alfabético, así que este se aplica después de mysqld.cnf:
sudo nano /etc/mysql/mysql.conf.d/99-replicacion.cnf
[mysqld]
server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
gtid_mode = ON
enforce_gtid_consistency = ON
bind-address = 0.0.0.0
Importante
enforce_gtid_consistencyrechaza las sentencias que no son seguras para GTID, como actualizar tablas InnoDB y MyISAM en la misma transacción. Si tu aplicación aún usa tablas MyISAM, pruébalo antes en un entorno de pruebas.
Reinicia MySQL y permite la conexión al puerto 3306 solo desde el destino:
sudo systemctl restart mysql
sudo ufw allow from new_server_ip to any port 3306 proto tcp
Crea el usuario de replicación con una contraseña robusta en lugar de your_strong_password:
sudo mysql -e "CREATE USER 'repl'@'new_server_ip' IDENTIFIED BY 'your_strong_password'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'new_server_ip';"
Haz el volcado inicial. --single-transaction obtiene una copia consistente sin bloquear las tablas InnoDB, y --set-gtid-purged=ON incluye la posición GTID desde la que debe continuar la réplica:
sudo mysqldump --single-transaction --routines --triggers --events \
--set-gtid-purged=ON --databases app_db > app_db.sql
Copia el volcado al destino con scp o rsync.
Configurar la réplica
En el destino, crea la configuración con otro server-id y en modo solo lectura, para que nadie escriba en la réplica por error:
sudo nano /etc/mysql/mysql.conf.d/99-replicacion.cnf
[mysqld]
server-id = 2
log_bin = /var/log/mysql/mysql-bin.log
gtid_mode = ON
enforce_gtid_consistency = ON
read_only = ON
Reinicia MySQL, vacía el historial GTID local (en un servidor recién instalado no contiene nada que necesites) e importa el volcado:
sudo systemctl restart mysql
sudo mysql -e "RESET MASTER;"
sudo mysql < app_db.sql
En MySQL 8.4 o posterior, RESET MASTER se sustituye por RESET BINARY LOGS AND GTIDS. El volcado solo contiene la base de datos: crea también en el destino el usuario de la aplicación con los mismos permisos que en el origen.
Conecta la réplica al origen e iníciala. SOURCE_SSL=1 cifra la conexión, necesario para el método de autenticación predeterminado de MySQL 8:
sudo mysql -e "CHANGE REPLICATION SOURCE TO SOURCE_HOST='old_server_ip', SOURCE_USER='repl', SOURCE_PASSWORD='your_strong_password', SOURCE_AUTO_POSITION=1, SOURCE_SSL=1; START REPLICA;"
Comprueba el estado de la replicación:
sudo mysql -e "SHOW REPLICA STATUS\G" | grep -E 'Replica_IO_Running|Replica_SQL_Running:|Seconds_Behind_Source|Last_.*Error:'
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 0
Last_IO_Error:
Last_SQL_Error:
Las dos primeras deben ser Yes. Si Last_IO_Error muestra un error de conexión, revisa el cortafuegos y el bind-address del origen.
Paso 3: Replicar una base de datos PostgreSQL
La replicación por streaming envía al destino el registro de transacciones (WAL) del origen, de modo que el destino es una copia exacta de todo el clúster, con todas las bases de datos y usuarios.
Configurar el origen
En el origen, crea el rol de replicación:
sudo -u postgres psql -c "CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'your_strong_password';"
Haz que PostgreSQL escuche en la IP pública. Edita el archivo de configuración:
sudo nano /etc/postgresql/16/main/postgresql.conf
listen_addresses = 'localhost,old_server_ip'
Los valores predeterminados de PostgreSQL 16 (wal_level = replica, max_wal_senders = 10) ya permiten la replicación. Autoriza la conexión del destino al final de pg_hba.conf:
sudo nano /etc/postgresql/16/main/pg_hba.conf
host replication replicator new_server_ip/32 scram-sha-256
Reinicia PostgreSQL y abre el puerto 5432 solo al destino:
sudo systemctl restart postgresql
sudo ufw allow from new_server_ip to any port 5432 proto tcp
Crear la réplica
En el destino, detén PostgreSQL y aparta el directorio de datos vacío de la instalación:
sudo systemctl stop postgresql
sudo mv /var/lib/postgresql/16/main /var/lib/postgresql/16/main.orig
Clona el origen con pg_basebackup. -R deja la réplica configurada para conectarse al origen, y -C -S migracion crea un slot de replicación para que el origen conserve el WAL que la réplica aún no ha recibido:
sudo -u postgres pg_basebackup -h old_server_ip -U replicator \
-D /var/lib/postgresql/16/main -X stream -C -S migracion -R -P -W
Introduce la contraseña del rol replicator cuando la pida. Al terminar, arranca PostgreSQL:
sudo systemctl start postgresql
Comprueba en el destino que está en modo réplica:
sudo -u postgres psql -c "SELECT pg_is_in_recovery();"
pg_is_in_recovery
-------------------
t
Y en el origen, que la réplica está conectada y al día:
sudo -u postgres psql -c "SELECT client_addr, state, sent_lsn, replay_lsn FROM pg_stat_replication;"
client_addr | state | sent_lsn | replay_lsn
---------------+-----------+------------+------------
new_server_ip | streaming | 0/3000148 | 0/3000148
state debe ser streaming, y sent_lsn y replay_lsn iguales o muy próximos.
Advertenciamientras exista el slot
migracion, el origen guardará todo el WAL que la réplica no haya confirmado. Si detienes la réplica durante días, el disco del origen puede llenarse. Elimina el slot en cuanto termines la migración (paso 4).
Paso 4: Hacer el corte final
Con archivos y base de datos sincronizados, el corte dura lo que tardes en seguir estos pasos. Antes, baja a 300 segundos el TTL de los registros DNS que vas a cambiar.
-
Detén las escrituras en el origen: activa el modo de mantenimiento de la aplicación o detén sus servicios y el cron (
sudo systemctl stop cron). -
Vacía la cola de lsyncd. Espera unos segundos a que el log no muestre actividad, detén el servicio y lanza una última pasada de rsync para confirmar que no queda nada pendiente. Con
--itemize-changesverás cada archivo que aún se transfiera:sudo systemctl stop lsyncd sudo rsync -aHAX --numeric-ids --delete --itemize-changes \ --exclude='cache/' --exclude='*.tmp' \ /var/www/ root@new_server_ip:/var/www/Usa las mismas exclusiones que en la configuración de lsyncd para no borrar ni copiar lo que dejaste fuera.
-
Promociona la base de datos. Con MySQL, pon el origen en solo lectura:
sudo mysql -e "SET GLOBAL super_read_only = ON;"Compara el GTID ejecutado en ambos servidores con
sudo mysql -e "SELECT @@GLOBAL.gtid_executed;". Cuando coincidan, desconecta la réplica y permite escrituras en el destino:sudo mysql -e "STOP REPLICA; RESET REPLICA ALL; SET GLOBAL read_only = OFF;"Quita también la línea
read_only = ONde99-replicacion.cnfen el destino para que no vuelva a activarse al reiniciar.Con PostgreSQL, detén el origen para que no acepte más escrituras y promociona el destino:
sudo systemctl stop postgresqlsudo -u postgres psql -c "SELECT pg_promote();"El primer comando va en el origen y el segundo en el destino.
pg_promote()devuelvety, a partir de ese momento,SELECT pg_is_in_recovery();devuelvef. -
Arranca la aplicación en el destino apuntando a la base de datos local y pruébala contra la IP nueva con tu archivo
hostslocal. -
Cambia el DNS a
new_server_ipy verifica la propagación condig +short A your_domain @1.1.1.1.
Las sesiones de usuario guardadas en archivos locales o en la memoria del servidor antiguo se perderán en el cambio y los usuarios tendrán que volver a iniciar sesión. Si eso no es aceptable, guárdalas en la base de datos o en un Redis compartido antes de migrar.
Paso 5: Verificar y limpiar
Comprueba que el número de archivos coincide en ambos servidores:
sudo find /var/www -type f | wc -l
sudo ssh root@new_server_ip 'find /var/www -type f | wc -l'
Para la base de datos, compara el número de filas de las tablas principales en el origen (ya en solo lectura o detenido, puedes arrancarlo solo para consultar) y en el destino, por ejemplo con SELECT COUNT(*) FROM orders;.
Cuando confirmes que el destino funciona, limpia todo lo que se creó para la sincronización:
- Elimina el slot de replicación de PostgreSQL en el origen, si vuelves a arrancarlo:
sudo -u postgres psql -c "SELECT pg_drop_replication_slot('migracion');". - Borra el usuario
replde MySQL o el rolreplicatorde PostgreSQL y cierra el puerto de la base de datos en el cortafuegos del origen (sudo ufw status numberedysudo ufw delete <número>). - Deshabilita lsyncd en el origen con
sudo systemctl disable lsyncd. - Quita la clave del origen de
/root/.ssh/authorized_keysen el destino.
Solución de problemas
- lsyncd se detiene con
Error: Terminating since out of inotify watches: subefs.inotify.max_user_watchescomo se indica en el paso 1 y reinicia el servicio. Replica_IO_Running: Connectingen MySQL: el destino no alcanza el puerto 3306 del origen. Compruebabind-address, el cortafuegos y prueba connc -vz old_server_ip 3306desde el destino.Last_SQL_Errorcon un error de clave duplicada: alguien escribió en la réplica o el volcado se importó sobre datos existentes. Vuelve a crear la réplica desde un volcado nuevo.pg_basebackup: error: connection ... no pg_hba.conf entry for replication connection: la línea depg_hba.confno coincide con la IP de origen de la conexión o falta reiniciar PostgreSQL.
Conclusión
Has mantenido un servidor nuevo sincronizado con el de producción: los archivos con lsyncd y la base de datos con replicación nativa de MySQL o PostgreSQL, y has reducido el corte a detener escrituras, promocionar la réplica y cambiar el DNS. Como siguientes pasos, ensaya el corte completo en un entorno de pruebas para cronometrarlo, y guarda un plan de vuelta atrás que indique cómo volver a sincronizar en sentido inverso si tuvieras que regresar al servidor antiguo.
