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 sudo en 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 datoMétodoRetraso típico
Archivos que cambian poco (código, configuración)rsync repetido a manoEl que tú decidas
Archivos que cambian continuamente (subidas de usuarios)lsyncd: inotify + rsyncUnos segundos
MySQL o MariaDBReplicación nativa con GTIDMenos de un segundo
PostgreSQLReplicación por streamingMenos 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.rsyncssh usa 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 = 5 agrupa los cambios de cinco segundos en una sola ejecución de rsync.
  • insist = true hace 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.

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

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.

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.

  1. 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).

  2. 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-changes verá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.

  3. 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 = ON de 99-replicacion.cnf en 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 postgresql
    
    sudo -u postgres psql -c "SELECT pg_promote();"
    

    El primer comando va en el origen y el segundo en el destino. pg_promote() devuelve t y, a partir de ese momento, SELECT pg_is_in_recovery(); devuelve f.

  4. Arranca la aplicación en el destino apuntando a la base de datos local y pruébala contra la IP nueva con tu archivo hosts local.

  5. Cambia el DNS a new_server_ip y verifica la propagación con dig +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 repl de MySQL o el rol replicator de PostgreSQL y cierra el puerto de la base de datos en el cortafuegos del origen (sudo ufw status numbered y sudo ufw delete <número>).
  • Deshabilita lsyncd en el origen con sudo systemctl disable lsyncd.
  • Quita la clave del origen de /root/.ssh/authorized_keys en el destino.

Solución de problemas

  • lsyncd se detiene con Error: Terminating since out of inotify watches: sube fs.inotify.max_user_watches como se indica en el paso 1 y reinicia el servicio.
  • Replica_IO_Running: Connecting en MySQL: el destino no alcanza el puerto 3306 del origen. Comprueba bind-address, el cortafuegos y prueba con nc -vz old_server_ip 3306 desde el destino.
  • Last_SQL_Error con 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 de pg_hba.conf no 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.