--- title: "Migrar MySQL y PostgreSQL a otro servidor sin downtime" description: "Migra una base de datos MySQL 8 o PostgreSQL 16 a otro servidor Ubuntu 24.04 con replicación, verifica los datos y cambia la aplicación en solo unos segundos." category: "Migración de Servidores" language: es source: https://cubepath.com/es/docs/migración-de-servidores/migracion-de-bases-de-datos-entre-servidores updated: 2026-09-25 --- # Migrar MySQL y PostgreSQL a otro servidor sin downtime La forma más simple de mover una base de datos es volcarla, copiarla e importarla, pero la aplicación tiene que estar parada todo ese tiempo, que con decenas de gigabytes puede ser más de una hora. Con replicación, el servidor nuevo recibe primero una copia completa y después aplica en tiempo real los cambios del antiguo, así que el corte se reduce a los segundos que tardas en apuntar la aplicación al servidor nuevo. En este tutorial migrarás una base de datos MySQL 8.0 con replicación por binlog y una PostgreSQL 16 con replicación lógica, ambas a Ubuntu 24.04, y verificarás los datos antes del cambio. ## Requisitos previos - **Servidor de origen** (`old_db_ip`): el que tiene hoy la base de datos, con MySQL 8.0 o PostgreSQL 16. - **Servidor de destino** (`new_db_ip`): Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con el mismo motor instalado (`sudo apt install mysql-server` o `sudo apt install postgresql`). Usa la misma versión mayor que el origen o una más reciente. - Un usuario no root con `sudo` en ambos servidores y conectividad entre ellos por red, preferiblemente privada, en el puerto 3306 (MySQL) o 5432 (PostgreSQL). - Espacio libre en el destino para al menos el doble del tamaño de los datos. - La base de datos se llama `your_db` en los ejemplos. Sigue la sección del motor que uses. Mira el tamaño de la base de datos para elegir método: ```bash sudo mysql -e "SELECT table_schema, ROUND(SUM(data_length+index_length)/1024/1024) AS MB FROM information_schema.tables GROUP BY table_schema" ``` ```bash sudo -u postgres psql -c "SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database" ``` | Método | Corte de servicio | Cuándo usarlo | |---|---|---| | Volcado e importación | Todo el tiempo de volcado, copia e importación | Bases pequeñas (unos pocos GB) o si puedes permitirte una ventana de mantenimiento | | Replicación | Segundos | Bases grandes o servicios que no pueden parar | > Nota: con el método de volcado basta con parar la aplicación, ejecutar `mysqldump --single-transaction --routines --triggers --events` o `pg_dump -Fc`, copiar el archivo e importarlo. El resto de esta guía describe la migración con replicación. ## Parte A: MySQL 8.0 ### Paso 1: Preparar el servidor de origen MySQL 8.0 activa el log binario por defecto, que es lo que usa la replicación. Compruébalo en el origen: ```bash sudo mysql -e "SELECT @@log_bin, @@server_id, @@bind_address" ``` ```output +-----------+-------------+----------------+ | @@log_bin | @@server_id | @@bind_address | +-----------+-------------+----------------+ | 1 | 1 | 127.0.0.1 | +-----------+-------------+----------------+ ``` En Ubuntu, MySQL solo escucha en localhost. Para que el destino pueda conectarse, edita la configuración: ```bash sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf ``` Cambia `bind-address` para que incluya la IP por la que llegará el destino: ```ini bind-address = 127.0.0.1,old_db_ip ``` Este cambio exige reiniciar MySQL, lo que supone unos segundos de corte. Hazlo en un momento de poco tráfico: ```bash sudo systemctl restart mysql sudo ss -ltnp | grep 3306 ``` Permite el puerto 3306 solo desde el servidor de destino: ```bash sudo ufw allow from new_db_ip to any port 3306 proto tcp ``` Crea el usuario de replicación. Usa una contraseña fuerte en lugar de `repl_password`: ```bash sudo mysql ``` ```sql CREATE USER 'repl'@'new_db_ip' IDENTIFIED BY 'repl_password'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'new_db_ip'; EXIT; ``` ### Paso 2: Volcar los datos con la posición del binlog `--source-data=2` añade al volcado, como comentario, el archivo y la posición del binlog exactos en los que se tomó la copia. La réplica empezará a leer cambios desde ese punto, así que no se pierde ni se duplica nada. `--single-transaction` hace la copia coherente sin bloquear las tablas InnoDB, salvo un bloqueo global muy breve al inicio para leer la posición: ```bash sudo mysqldump --single-transaction --source-data=2 --routines --triggers --events \ --databases your_db | gzip > ~/your_db.sql.gz ``` Consulta la posición guardada: ```bash zcat ~/your_db.sql.gz | grep -m1 -E 'CHANGE (REPLICATION SOURCE|MASTER) TO' ``` ```output -- CHANGE REPLICATION SOURCE TO SOURCE_LOG_FILE='binlog.000004', SOURCE_LOG_POS=1257; ``` Anota el archivo y la posición. Copia el volcado al servidor de destino: ```bash scp ~/your_db.sql.gz your_user@new_db_ip:~/ ``` ### Paso 3: Configurar el servidor de destino como réplica Cada servidor de una replicación necesita un `server-id` distinto. En el destino, edita la configuración y añade estas líneas al final de la sección `[mysqld]`: ```bash sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf ``` ```ini server-id = 2 read_only = ON ``` `read_only` evita que la aplicación o un usuario escriban por error en la réplica; el propio proceso de replicación no se ve afectado. Reinicia e importa el volcado. Como se creó con `--databases`, incluye la sentencia `CREATE DATABASE`: ```bash sudo systemctl restart mysql zcat ~/your_db.sql.gz | sudo mysql ``` Crea en el destino los usuarios de la aplicación. `--databases` no copia la tabla de usuarios, así que tienes que crearlos con sus mismos permisos. Puedes ver los que tiene el origen con `SHOW GRANTS FOR 'your_db_user'@'host';`. Conecta la réplica al origen usando el archivo y la posición del paso anterior. `GET_SOURCE_PUBLIC_KEY=1` es necesario porque MySQL 8 usa por defecto `caching_sha2_password` y la conexión no va cifrada con TLS: ```bash sudo mysql ``` ```sql CHANGE REPLICATION SOURCE TO SOURCE_HOST='old_db_ip', SOURCE_USER='repl', SOURCE_PASSWORD='repl_password', SOURCE_LOG_FILE='binlog.000004', SOURCE_LOG_POS=1257, GET_SOURCE_PUBLIC_KEY=1; START REPLICA; ``` Comprueba el estado: ```sql SHOW REPLICA STATUS\G ``` ```output Replica_IO_Running: Yes Replica_SQL_Running: Yes Seconds_Behind_Source: 0 Last_IO_Error: Last_SQL_Error: ``` Los dos hilos deben estar en `Yes`. Si `Seconds_Behind_Source` es alto, la réplica está recuperando los cambios acumulados durante la importación; espera a que baje a 0. ### Paso 4: Verificar los datos Con la réplica al día, compara las tablas más importantes. `CHECKSUM TABLE` calcula una suma del contenido; ejecútalo en ambos servidores y compara los resultados: ```sql CHECKSUM TABLE your_db.orders, your_db.users; ``` ```output +----------------+------------+ | Table | Checksum | +----------------+------------+ | your_db.orders | 2863184502 | | your_db.users | 917552311 | +----------------+------------+ ``` En tablas que reciben escrituras continuamente los valores pueden diferir durante unos instantes; repite la comprobación o hazla durante el cambio del paso 5, cuando el origen ya no acepta escrituras. ### Paso 5: Cambiar la aplicación al servidor nuevo Este es el único momento con impacto, y dura segundos. Prepara de antemano la configuración de la aplicación con la nueva IP para aplicarla rápido. En el **origen**, bloquea las escrituras. `super_read_only` impide escribir incluso a usuarios con privilegios de administrador: ```sql SET GLOBAL super_read_only = ON; SHOW MASTER STATUS; ``` ```output +---------------+----------+ | File | Position | +---------------+----------+ | binlog.000004 | 982231 | +---------------+----------+ ``` En MySQL 8.4 y posteriores, el comando equivalente es `SHOW BINARY LOG STATUS`. En el **destino**, espera a que la réplica haya ejecutado hasta esa misma posición: ```sql SHOW REPLICA STATUS\G ``` ```output Relay_Source_Log_File: binlog.000004 Exec_Source_Log_Pos: 982231 ``` Cuando coincidan, desconecta la réplica y permite escrituras: ```sql STOP REPLICA; RESET REPLICA ALL; SET GLOBAL read_only = OFF; ``` Quita también la línea `read_only = ON` de `/etc/mysql/mysql.conf.d/mysqld.cnf` en el destino para que no se active en el próximo reinicio. Ahora apunta la aplicación a `new_db_ip`, reiníciala si es necesario y comprueba en sus logs que escribe correctamente. Deja el servidor de origen en `super_read_only` unos días como referencia y para poder comparar datos, y después retíralo tras una última copia de seguridad. ## Parte B: PostgreSQL 16 PostgreSQL ofrece replicación lógica, que copia los cambios tabla a tabla y funciona entre versiones mayores distintas (por ejemplo, de PostgreSQL 14 a 16). Tiene dos limitaciones que debes conocer: no replica cambios de esquema (DDL) ni los valores de las secuencias. Evita cambios de esquema durante la migración; las secuencias se sincronizan en el paso de cambio. ### Paso 1: Preparar el servidor de origen Edita la configuración principal del origen. En Ubuntu está en `/etc/postgresql//main/`: ```bash sudo nano /etc/postgresql/16/main/postgresql.conf ``` ```ini listen_addresses = 'localhost,old_db_ip' wal_level = logical ``` Permite que el usuario de replicación se conecte a la base de datos desde el destino. La replicación lógica usa una entrada normal con el nombre de la base de datos, no la palabra `replication`: ```bash sudo nano /etc/postgresql/16/main/pg_hba.conf ``` ```text host your_db repl new_db_ip/32 scram-sha-256 ``` Cambiar `wal_level` requiere reiniciar PostgreSQL, unos segundos de corte: ```bash sudo systemctl restart postgresql sudo -u postgres psql -Atc "SHOW wal_level" sudo ufw allow from new_db_ip to any port 5432 proto tcp ``` ```output logical ``` Crea el usuario de replicación con permiso de lectura sobre las tablas y la publicación, que define qué tablas se replican: ```bash sudo -u postgres psql -d your_db ``` ```sql CREATE ROLE repl WITH LOGIN REPLICATION PASSWORD 'repl_password'; GRANT USAGE ON SCHEMA public TO repl; GRANT SELECT ON ALL TABLES IN SCHEMA public TO repl; CREATE PUBLICATION migracion FOR ALL TABLES; ``` Si usas otros esquemas además de `public`, repite los `GRANT` para cada uno. Para replicar `UPDATE` y `DELETE`, cada tabla necesita una clave primaria. Busca las que no la tienen: ```sql SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r' AND n.nspname NOT IN ('pg_catalog', 'information_schema') AND NOT EXISTS ( SELECT 1 FROM pg_constraint p WHERE p.conrelid = c.oid AND p.contype = 'p' ); ``` Para cada tabla que aparezca, añade una clave primaria o, si no es posible, ejecuta `ALTER TABLE nombre_tabla REPLICA IDENTITY FULL;` (funciona, pero es más lento en tablas grandes). ### Paso 2: Copiar roles y esquema al destino La replicación lógica necesita que las tablas ya existan en el destino. En el origen, exporta los roles y el esquema (sin datos): ```bash sudo -u postgres pg_dumpall --roles-only -f /tmp/roles.sql sudo -u postgres pg_dump --schema-only --no-publications --no-subscriptions -d your_db -f /tmp/your_db-schema.sql ``` En el destino, cópialos y aplícalos: ```bash scp your_user@old_db_ip:/tmp/roles.sql your_user@old_db_ip:/tmp/your_db-schema.sql /tmp/ sudo -u postgres psql -f /tmp/roles.sql sudo -u postgres createdb -O your_db_owner your_db sudo -u postgres psql -d your_db -f /tmp/your_db-schema.sql ``` Sustituye `your_db_owner` por el propietario de la base de datos en el origen. El error `role "postgres" already exists` al importar los roles es normal. Borra después los archivos de `/tmp` de ambos servidores, porque `roles.sql` contiene los hashes de las contraseñas. ### Paso 3: Crear la suscripción En el destino, crea la suscripción. PostgreSQL copia primero todos los datos existentes y después aplica los cambios en tiempo real: ```bash sudo -u postgres psql -d your_db ``` ```sql CREATE SUBSCRIPTION migracion CONNECTION 'host=old_db_ip port=5432 dbname=your_db user=repl password=repl_password' PUBLICATION migracion; ``` ```output NOTICE: created replication slot "migracion" on publisher CREATE SUBSCRIPTION ``` Sigue el progreso de la copia inicial. Cada tabla pasa por varios estados hasta `r` (ready): ```sql SELECT srrelid::regclass AS tabla, srsubstate FROM pg_subscription_rel ORDER BY 2; ``` ```output tabla | srsubstate -------------+------------ orders | r users | r ``` Cuando todas estén en `r`, compara el número de filas de las tablas principales en ambos servidores: ```sql SELECT count(*) FROM orders; ``` ### Paso 4: Cambiar la aplicación al servidor nuevo Detén la aplicación o ponla en modo mantenimiento para que no haya escrituras en el origen. Comprueba en el origen que la réplica ha confirmado todo el WAL. Las dos columnas deben coincidir: ```bash sudo -u postgres psql -Atc "SELECT confirmed_flush_lsn, pg_current_wal_lsn() FROM pg_replication_slots WHERE slot_name = 'migracion'" ``` ```output 0/5A3F2C8|0/5A3F2C8 ``` Las secuencias no se replican, así que en el destino siguen en su valor inicial y los siguientes `INSERT` fallarían por clave duplicada. Genera en el origen los comandos `setval` con los valores actuales: ```bash sudo -u postgres psql -d your_db -Atc "SELECT format('SELECT setval(%L, %s, true);', quote_ident(schemaname) || '.' || quote_ident(sequencename), last_value) FROM pg_sequences WHERE last_value IS NOT NULL" > /tmp/sequences.sql ``` Cópialo al destino y aplícalo: ```bash scp your_user@old_db_ip:/tmp/sequences.sql /tmp/ sudo -u postgres psql -d your_db -f /tmp/sequences.sql ``` Elimina la suscripción en el destino. Esto también borra el slot de replicación del origen, que de lo contrario seguiría acumulando WAL y llenaría el disco: ```bash sudo -u postgres psql -d your_db -c "DROP SUBSCRIPTION migracion" ``` Apunta la aplicación a `new_db_ip`, arráncala y comprueba que puede leer y escribir. Por último, en el origen, elimina la publicación: ```bash sudo -u postgres psql -d your_db -c "DROP PUBLICATION migracion" ``` ## Plan de vuelta atrás Mientras no hayas escrito en el servidor nuevo, volver atrás consiste en revertir la configuración de la aplicación y desbloquear el origen (`SET GLOBAL super_read_only = OFF;` en MySQL, o arrancar de nuevo la aplicación en PostgreSQL). Si el servidor nuevo ya ha recibido escrituras, tendrías que copiarlas al origen, así que decide si vuelves atrás en los primeros minutos tras el cambio. Conserva el servidor de origen intacto hasta que la aplicación lleve varios días estable en el nuevo. ## Solución de problemas - **MySQL: `Replica_IO_Running: Connecting` con `Last_IO_Error` de autenticación**: falta `GET_SOURCE_PUBLIC_KEY=1`, la contraseña no coincide o el usuario `repl` se creó con otra IP de origen. Prueba la conexión desde el destino con `mysql -h old_db_ip -u repl -p --get-server-public-key`. - **MySQL: `Replica_SQL_Running: No` con error 1062 (entrada duplicada)**: la posición del binlog usada no corresponde al volcado importado. Repite el volcado y la importación con la posición correcta. - **PostgreSQL: `could not connect to the publisher`**: revisa `listen_addresses`, la línea de `pg_hba.conf` (y recárgalo con `sudo systemctl reload postgresql`) y el firewall del origen. - **PostgreSQL: `permission denied for table`** durante la copia inicial: al usuario `repl` le falta `SELECT` sobre alguna tabla o esquema. - **PostgreSQL: `duplicate key value violates unique constraint` tras el cambio**: no se sincronizaron las secuencias. Repite la generación y aplicación de `setval`. ## Conclusión Has migrado la base de datos a un servidor nuevo manteniéndola sincronizada con el origen mientras verificabas los datos, y has limitado el corte a los segundos de bloquear escrituras y cambiar la conexión de la aplicación. Como siguientes pasos, configura copias de seguridad automáticas en el servidor nuevo, prueba una restauración y, si el servidor migrado es solo la base de datos de un sitio web, consulta la guía de migración de sitios web sin downtime para mover también los archivos.