Migrar WordPress a un nuevo servidor, sin caídas

Publicado el 17 min de lectura

El orden lo decide todo: prepara el servidor nuevo, copia los datos, prueba bajo el dominio real con el archivo hosts, emite el certificado por adelantado y solo después cambia el DNS. Con camino de vuelta y mensajes de error reales.

Una migración de WordPress rara vez falla por culpa de un comando concreto. Falla por el orden. Quien cambia primero el DNS y copia después tiene garantizada una ventana en la que los visitantes ven una página de error o una instalación a medio llenar. Si lo haces al revés, puedes terminar el servidor nuevo con calma, probarlo bajo el dominio real y cambiar el registro DNS justo en el momento en que todo funciona de forma demostrable.

Este artículo describe exactamente ese orden y, además, los puntos donde la cosa revienta en la práctica: datos serializados en la base de datos, collations al pasar de MySQL a MariaDB, certificados sin un registro DNS que apunte al servidor nuevo y el camino de vuelta por si se te ha escapado algo.

La secuencia que no genera ninguna caída

La web antigua sigue online hasta el último momento. No se apaga nada, no se borra nada. El servidor nuevo funciona en paralelo, se prueba y solo toma el relevo cuando el registro DNS ya está cambiado.

  1. Baja el TTL de los registros A y AAAA a 300 segundos, como mínimo 24 o 48 horas antes.
  2. Prepara el servidor nuevo: servidor web, PHP, base de datos, usuarios, directorios.
  3. Copia los archivos, copia la base de datos.
  4. Emite el certificado para el dominio, aunque el DNS siga apuntando al servidor antiguo.
  5. Ajusta el archivo hosts en tu propio equipo y recorre la web bajo el dominio real.
  6. Justo antes del corte, lanza una sincronización delta para que lleguen los últimos cambios.
  7. Cambia el DNS. Deja el servidor antiguo funcionando varios días más.

El único punto en el que teóricamente puede aparecer un hueco es el paso 6. Lo pequeño que sea ese hueco lo decide el paso 1.

Bajar el TTL antes de tocar cualquier otra cosa

El TTL les dice a los resolvers de todo el mundo cuánto tiempo pueden guardar una respuesta en caché. Si está en 86400, un resolver puede seguir entregando tu IP antigua durante 24 horas después de que hayas hecho el cambio. Y hay un detalle que se pasa por alto con facilidad: un TTL rebajado solo surte efecto cuando ha expirado el TTL anterior. Si pasas de 86400 a 300, puede tardar un día entero hasta que todos los resolvers conozcan el valor corto.

Por eso este es el primer paso y no el último. dig no viene de serie en ninguna distribución y se usa a lo largo de toda esta guía, así que instálalo primero:

apt install -y bind9-dnsutils

En Debian 11, Ubuntu 22.04 y Ubuntu 24.04 el paquete todavía se llama dnsutils. A partir de Debian 12, dnsutils es solo un marcador que remite a bind9-dnsutils, y en Debian 13 es incluso un paquete puramente virtual sin versión propia. Los dos funcionan, porque apt resuelve por su cuenta el único proveedor, pero bind9-dnsutils es el nombre que va a permanecer. Después comprueba el valor actual:

dig +noall +answer example.com A
dig +noall +answer example.com SOA

El número de la segunda columna de la respuesta A es el TTL restante. Al servidor autoritativo pregúntale directamente, porque si no solo ves el valor residual que queda en la caché de tu propio resolver:

dig @a.ns14.net example.com A +noall +answer

Después de la migración vuelve a subir el TTL, a 3600 o más. Los TTL cortos de forma permanente generan carga innecesaria y alargan las caídas de tu proveedor de DNS.

Equipar el servidor nuevo a la medida

Aquí es donde surgen la mayoría de las sorpresas desagradables, porque la distribución del servidor nuevo entrega versiones distintas a las del antiguo. Todos los comandos de paquetes de esta guía dan por hecho Debian o Ubuntu; en AlmaLinux, Rocky Linux y Oracle Linux no existe apt y los nombres de los paquetes cambian. A fecha de julio de 2026 el panorama es este:

SistemaPHPBase de datosnginx
Debian 138.4MariaDB 11.8 (sin mysql-server)1.26.3
Debian 128.2MariaDB 10.111.22.1
Ubuntu 24.048.3MySQL 8.0.46 o MariaDB 10.111.24.0
Ubuntu 22.048.1MySQL 8.0.46 o MariaDB 10.61.18.0

De ahí se derivan dos consecuencias. La primera: en Debian no vas a obtener mysql-server, allí siempre corre MariaDB. Si la web antigua funcionaba sobre MySQL 8, migrar a Debian es al mismo tiempo un cambio de base de datos, con una trampa muy concreta más abajo. La segunda: el salto de PHP 8.1 a 8.4 no es automático. Los temas y plugins antiguos reaccionan a las funciones eliminadas con un Fatal error: Uncaught Error: Call to undefined function o con una página en blanco. Si la instalación antigua corría sobre PHP 8.1 y no quieres acoplar la migración a una actualización de PHP, Ubuntu 22.04 o una versión de PHP salida de un repositorio externo es la opción más tranquila. Lo que falta a propósito en la tabla es Debian 11: allí php-cli todavía entrega PHP 7.4.33, es decir, una versión sin mantenimiento de seguridad y por debajo de lo que recomienda WordPress. Eso descarta Debian 11 como destino de una migración, salvo que integres antes el repositorio Sury.

Los paquetes básicos para una instalación típica con nginx y PHP-FPM:

apt update
apt install -y nginx php-fpm php-mysql php-xml php-curl php-mbstring php-zip php-gd php-intl
apt install -y mariadb-server mariadb-client rsync curl

Los detalles sobre el servidor web están en la guía de nginx, y la protección básica de la base de datos en asegurar MariaDB y MySQL. Si el servidor está recién creado, antes merece la pena echar un vistazo a la checklist para servidores root nuevos.

Crea la base de datos y el usuario con el mismo juego de caracteres que en el origen:

CREATE DATABASE wp_nuevo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_nuevo'@'localhost' IDENTIFIED BY 'una-contrasena-larga';
GRANT ALL PRIVILEGES ON wp_nuevo.* TO 'wp_nuevo'@'localhost';
FLUSH PRIVILEGES;

Transferir los archivos sin destrozar los permisos

La copia se hace directamente de servidor a servidor, no pasando por tu conexión de casa. Lo más sencillo es rsync del servidor antiguo al nuevo, con una clave SSH en lugar de contraseña:

rsync -az --delete --exclude 'wp-content/cache/' --exclude 'wp-content/uploads/backup*' \
  -e 'ssh -p 22' /var/www/html/ root@nueva.ip:/var/www/html/

La primera pasada puede tardar. Justo por eso se hace días antes del corte, y poco antes se ejecuta una vez más el mismo comando, que entonces solo transfiere las diferencias.

Después de copiar hay que dejar en orden propietario y permisos, porque los UID difieren entre sistemas y rsync transfiere números, no nombres:

chown -R www-data:www-data /var/www/html
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
chmod 640 /var/www/html/wp-config.php

Dos archivos se retiran antes del primer arranque, si es que existen: wp-content/object-cache.php y wp-content/advanced-cache.php. Los dos son drop-ins de plugins de caché que apuntan a un socket de Redis o Memcached que en el servidor nuevo todavía no existe. El resultado sería una página en blanco sin ningún mensaje aprovechable.

Transferir la base de datos y la trampa de las collations

El dump se hace con WP-CLI o de la forma clásica. WP-CLI tiene la ventaja de que toma el juego de caracteres y el prefijo del wp-config.php:

cd /var/www/html
wp db export /root/wp-dump.sql --add-drop-table

Sin WP-CLI, en un sistema MariaDB actual la herramienta se llama mariadb-dump. El nombre antiguo mysqldump es desde MariaDB 11.0 solo un enlace simbólico obsoleto y te recibe con Deprecated program name. It will be removed in a future release:

mariadb-dump --single-transaction --default-character-set=utf8mb4 wp_antiguo > /root/wp-dump.sql

Y ahora el punto en el que una migración de Ubuntu con MySQL 8 a Debian con MariaDB se da de bruces con regularidad. MySQL 8 usa por defecto la collation utf8mb4_0900_ai_ci, que en MariaDB no existe en absoluto. La importación se interrumpe con:

ERROR 1273 (HY000) at line 42: Unknown collation: 'utf8mb4_0900_ai_ci'

La reparación va antes de la importación, directamente en el dump:

sed -i 's/utf8mb4_0900_ai_ci/utf8mb4_unicode_ci/g; s/utf8mb4_0900_as_cs/utf8mb4_unicode_ci/g' /root/wp-dump.sql
grep -c utf8mb4_unicode_ci /root/wp-dump.sql

Después importa:

mariadb --default-character-set=utf8mb4 wp_nuevo < /root/wp-dump.sql

Si después de la importación aparece por todas partes información en lugar de información, el juego de caracteres se ha perdido en el dump o en la importación. Eso no se arregla buscando y reemplazando, sino repitiendo dump e importación con --default-character-set=utf8mb4. Precisamente por eso la base de datos antigua sigue intacta en este momento.

A continuación ajusta los datos de acceso en el wp-config.php del servidor nuevo: DB_NAME, DB_USER, DB_PASSWORD y, muy importante, DB_HOST si la instalación antigua apuntaba a un servidor de base de datos remoto. Si ahí se queda la dirección antigua, verás Error establishing a database connection aunque en local funcione todo. Si la conexión se atasca a pesar de que los datos son correctos, te ayuda solucionar Access denied for user.

Reemplazar el dominio, y por qué un REPLACE en SQL destroza la web

Primero la buena noticia: si solo cambia el servidor y el dominio se mantiene, no tienes que reemplazar absolutamente nada en la base de datos. Esa es justo una de las razones por las que la prueba con el archivo hosts es superior a la prueba con un dominio provisional. Reemplazar solo hace falta si el dominio cambia de verdad, si unes la migración con el paso de http a https o si al final trabajas con un dominio de pruebas y luego tienes que deshacer el reemplazo.

Y ahora la razón por la que eso no se hace nunca con una simple sentencia SQL. WordPress guarda opciones, ajustes de widgets y configuraciones de temas como arrays PHP serializados. Una URL no está ahí desnuda, sino con su longitud en bytes delante:

s:19:"https://example.com"

Si reemplazas example.com por dominio-nuevo.com mediante UPDATE ... REPLACE(), la cadena se alarga pero el número 19 se queda donde estaba. PHP ya no puede desempaquetar el array, unserialize() devuelve false y el ajuste afectado desaparece. Síntomas típicos: widgets desaparecidos, opciones del tema reiniciadas, entradas de menú vacías y, en los logs, Notice: unserialize(): Error at offset.

WP-CLI lo resuelve bien, porque desempaqueta los datos, los reemplaza y los vuelve a escribir con la longitud corregida. Empieza siempre con --dry-run:

wp search-replace 'https://dominio-antiguo.com' 'https://dominio-nuevo.com' --all-tables --precise --dry-run --report-changed-only

Lo que hace cada parámetro: --all-tables toca también las tablas que no siguen el prefijo de WordPress, por ejemplo las de plugins de tienda o de formularios. --precise fuerza el procesamiento en PHP en lugar de en SQL, es más lento, pero trata los datos serializados de forma fiable. --report-changed-only reduce la salida a las tablas con coincidencias reales.

Si la vista previa parece plausible, ejecuta el mismo comando sin --dry-run. Después llega la pasada que casi todas las guías se saltan: los page builders como Elementor guardan sus contenidos como JSON en la base de datos, y ahí las barras van escapadas. Un reemplazo de https://dominio-antiguo.com sencillamente no encuentra https:\/\/dominio-antiguo.com. De ahí una segunda pasada:

wp search-replace 'https:\/\/dominio-antiguo.com' 'https:\/\/dominio-nuevo.com' --all-tables --precise --report-changed-only

Con Elementor toca además regenerar los archivos CSS en el backend, dentro de Herramientas; si no, las hojas de estilo generadas seguirán apuntando al dominio antiguo.

Dos cosas a las que search-replace no llega: las constantes del wp-config.php y multisitio. Si ahí pone define('WP_HOME', 'https://dominio-antiguo.com');, eso sobrescribe cualquier valor de la base de datos y te pasas horas buscando en el sitio equivocado. En multisitio necesitas además --network y tienes que ajustar los dominios en wp_blogs y wp_site.

Y una advertencia sobre el prefijo de tablas: no lo cambies durante la migración. El prefijo también está metido en el nombre de opción wp_user_roles y en los campos meta de usuario wp_capabilities y wp_user_level. Quien renombra solo las tablas todavía puede iniciar sesión, pero después recibe Sorry, you are not allowed to access this page. y se queda sin administrador.

Emitir el certificado antes de que el DNS apunte al nuevo servidor

Aquí hay un problema del huevo y la gallina. La validación HTTP habitual de Let's Encrypt exige que el nombre de dominio apunte ya al servidor que valida. Y en este momento no lo hace, porque todavía tiene que servir la web antigua. Quien aun así lanza certbot --nginx obtiene:

Certbot failed to authenticate some domains (authenticator: nginx).
Invalid response from http://example.com/.well-known/acme-challenge/...: 404

La salida limpia es la validación por DNS. Comprueba un registro TXT _acme-challenge.example.com y le da igual a dónde apunte el registro A:

certbot certonly --manual --preferred-challenges dns -d example.com -d www.example.com

Certbot muestra un valor que tienes que dar de alta como registro TXT en tu zona DNS. Antes de confirmar, comprueba tú mismo si la zona ya está entregando ese registro, porque si no quemas un intento fallido:

dig +short TXT _acme-challenge.example.com

Para dominios wildcard, o si quieres automatizarlo, el camino a través de la API de DNS está descrito en el artículo sobre el certificado wildcard. La variante manual no se renueva sola, así que después del cambio de DNS pásate a la validación HTTP normal. A partir de ese momento funciona, porque el dominio ya apunta al servidor nuevo.

Probar con el archivo hosts, como un visitante real

Ahora llega la parte que marca la diferencia entre "debería funcionar" y "funciona". Rediriges solo tu propio equipo al servidor nuevo, mientras el resto del mundo sigue viendo la web antigua.

En Linux y macOS editas /etc/hosts como root; en Windows, C:\Windows\System32\drivers\etc\hosts en un editor abierto como administrador. Añade una línea con la IP del servidor nuevo:

203.0.113.10 example.com www.example.com

Después vacía la caché de DNS, porque si no el cambio no surte efecto de inmediato:

resolvectl flush-caches

En Windows, ipconfig /flushdns; en macOS, sudo dscacheutil -flushcache seguido de sudo killall -HUP mDNSResponder. Comprueba si ha surtido efecto:

getent hosts example.com

Una trampa que cuesta muchas horas: Firefox con DNS sobre HTTPS activado ignora el archivo hosts. Mozilla lo ha marcado expresamente como "wontfix". Entonces sigues viendo la web antigua y das la prueba por fallida, aunque el servidor nuevo lleve rato respondiendo correctamente. Por eso, desactiva la resolución DNS cifrada en el navegador de pruebas o, mejor todavía, comprueba sin depender del navegador. curl puede sobrescribir la resolución en cada llamada, sin necesidad de archivo hosts:

curl -sI --resolve example.com:443:203.0.113.10 https://example.com/

Lo que deberías repasar en este estado: la portada, al menos dos páginas internas, una entrada con imágenes, el acceso en /wp-login.php, el backend, un formulario de contacto y, si hay tienda, un artículo de prueba hasta la caja. Las imágenes merecen atención especial, porque las subidas que faltan no se notan en el backend.

Lo que la prueba con hosts no cubre en absoluto: todo lo que accede al servidor desde fuera. Los webhooks de los proveedores de pago, los cronjobs externos, los rastreadores de los buscadores y tu envío de correo siguen llegando al destino antiguo. Eso está bien, solo tienes que saberlo.

El corte: sincronización delta y DNS

Entre la primera copia y el cambio se han añadido comentarios, pedidos o entradas. El orden para un corte limpio:

  1. Activa el modo de mantenimiento en el servidor antiguo o, como mínimo, detén un momento pedidos y comentarios.
  2. Sincroniza los archivos otra vez, esta vez tarda segundos.
  3. Exporta la base de datos de nuevo e impórtala en el servidor nuevo.
  4. Vacía las cachés: wp cache flush y wp rewrite flush --hard.
  5. Comprueba otra vez con curl contra la IP nueva.
  6. Cambia los registros A y AAAA.

No te olvides del registro AAAA. Si se queda en la dirección IPv6 antigua, todos los visitantes con IPv6 seguirán aterrizando en el servidor antiguo mientras los visitantes por IPv4 ven la web nueva. Eso produce justo el tipo de fallo en el que dos personas sentadas una al lado de la otra ven webs distintas.

Deja el servidor antiguo funcionando al menos una semana, sin cambios. Es tu camino de vuelta y tu archivo.

Cómo saber que de verdad ha salido bien

No vale con "la web carga", hacen falta pruebas medibles:

dig +short A example.com
dig +short AAAA example.com
curl -sI https://example.com/ | head -n 12

En las cabeceras no puede aparecer ningún 301 hacia un dominio antiguo ni hacia http://. Después pregúntale a la base de datos qué dirección considera correcta el propio WordPress:

wp option get siteurl
wp option get home

Una prueba fiable para detectar restos olvidados es una pasada de búsqueda en modo seco. Si informa de cero reemplazos, la dirección antigua ya no está guardada en ningún sitio:

wp search-replace 'dominio-antiguo.com' 'dominio-antiguo.com' --all-tables --dry-run --report-changed-only

Además, revisa el certificado, incluidos el periodo de validez y los nombres que cubre:

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates -subject

Y, por último, el log de errores del servidor web, mientras te paseas cinco minutos por la web. Si ahí no aparece nada nuevo, ya está. Si algo se queda colgado, solucionar el 502 Bad Gateway es el punto de partida más frecuente, casi siempre porque el socket de PHP-FPM en el bloque de nginx todavía lleva la ruta de la versión antigua de PHP.

Si sale mal: el camino de vuelta

El camino de vuelta consiste en un solo movimiento, y por eso el paso 1 era tan importante: devuelve los registros A y AAAA a la IP antigua. Con un TTL de 300 segundos, la mayoría de los visitantes vuelve al servidor antiguo en cinco minutos, y ese servidor ha seguido funcionando sin cambios.

Hay exactamente un caso en el que esto no queda limpio: cuando en el servidor nuevo ya se han generado datos que en el antiguo no existen. Pedidos nuevos, comentarios, registros. Después de una vuelta atrás, esos datos solo existen en el sistema nuevo. Por eso se decide dentro de los primeros minutos o no se decide. Y por eso el modo de mantenimiento durante el corte: mantiene pequeña la ventana en la que los dos conjuntos de datos pueden separarse.

Si aun así tienes que volver más tarde, exporta del servidor nuevo solo las tablas afectadas y cárgalas en el antiguo, en lugar de devolver la base de datos completa. Una vuelta atrás pura desde una copia de seguridad completa antigua borra todo lo que ha pasado mientras tanto.

Mensajes de error, literales

Error establishing a database connection
Los datos de acceso o DB_HOST del wp-config.php no encajan con el servidor nuevo. A menudo sigue ahí la IP de un servidor de base de datos externo en lugar de localhost.

ERROR 1273 (HY000): Unknown collation: 'utf8mb4_0900_ai_ci'
Un dump de MySQL 8 se está importando en MariaDB. Pon la collation del dump a utf8mb4_unicode_ci con sed.

Error: This does not seem to be a WordPress installation.
WP-CLI se ha lanzado en el directorio equivocado o los archivos no están donde supones. Trabaja con --path=/var/www/html.

ERR_TOO_MANY_REDIRECTS
Casi siempre una contradicción entre siteurl en la base de datos, una constante del wp-config.php y la redirección del servidor web. Además afecta a las instalaciones detrás de un proxy que no se enteran del estado HTTPS y por eso mandan sin parar de http a https y de vuelta.

La portada carga, todas las páginas internas devuelven 404
El clásico al pasar de Apache a nginx. El .htaccess con las reglas de los enlaces permanentes lo ignora nginx; ahí hace falta try_files $uri $uri/ /index.php?$args; en el bloque location.

El archivo subido excede la directiva upload_max_filesize en php.ini.
La configuración de PHP no se ha migrado con el resto. Pon upload_max_filesize, post_max_size y memory_limit en los valores del servidor antiguo.

Página en blanco, ninguna entrada en el log
Casi siempre un drop-in que apunta al vacío: wp-content/object-cache.php o advanced-cache.php. Renómbralo y vuelve a cargar.

Una migración que transcurre así no tiene ninguna ventana sin web. El único momento perceptible es el cambio de DNS, y su duración la fijaste tú mismo dos días antes.

Preguntas frecuentes

¿Cuánto tarda el cambio de DNS en aplicarse en todas partes?
Depende exclusivamente del TTL que estuviera vigente antes del cambio. Con 300 segundos, prácticamente todos los resolvers han basculado en cinco o diez minutos. Con 86400 puede tardar un día entero. Como un TTL rebajado solo surte efecto cuando ha expirado el anterior, bájalo 24 o 48 horas antes de la migración.
¿Por qué no puedo reemplazar el dominio en la base de datos con una simple sentencia SQL?
WordPress guarda muchos ajustes como arrays PHP serializados, en los que cada texto va precedido de su longitud en bytes, por ejemplo s:19:"https://example.com". Un REPLACE() cambia el texto, no la longitud indicada. PHP ya no puede desempaquetar el array y el ajuste se pierde. WP-CLI deserializa, reemplaza y vuelve a serializar, por eso wp search-replace es el camino correcto.
¿Tengo que reemplazar el dominio si lo único que cambia es el servidor?
No. Si el dominio se mantiene igual, en la base de datos no cambia nada. Reemplazar solo hace falta en un cambio real de dominio, al pasar a la vez de http a https o si has trabajado con un dominio de pruebas y después tienes que deshacer el reemplazo.
¿Cómo consigo un certificado Let's Encrypt si el dominio todavía apunta al servidor antiguo?
Con la validación DNS en lugar de la validación HTTP: certbot certonly --manual --preferred-challenges dns -d example.com. Lo que se comprueba es un registro TXT bajo _acme-challenge, y el registro A no interviene. Después del cambio de DNS pásate a la validación HTTP automática para que la renovación funcione sin trabajo manual.
He cambiado el archivo hosts, pero sigo viendo la web antigua. ¿A qué se debe?
O la caché DNS del sistema operativo sigue caliente, y entonces ayudan ipconfig /flushdns, resolvectl flush-caches o dscacheutil -flushcache. O tu navegador resuelve por su cuenta con DNS sobre HTTPS. Está comprobado que Firefox ignora el archivo hosts en ese modo. La comprobación fiable es la de curl con --resolve, que se salta el navegador por completo.
¿Cuál es la vuelta atrás más rápida si algo no funciona después del cambio?
Devolver los registros A y AAAA a la IP antigua. El servidor antiguo sigue funcionando sin cambios. Con un TTL corto, la mayoría de los visitantes vuelve allí en pocos minutos. El inconveniente: los datos generados mientras tanto solo en el servidor nuevo, por ejemplo pedidos, faltarán en el antiguo. Por eso se mantiene pequeña la ventana con un breve modo de mantenimiento y se decide pronto.

WordPress Migración de servidores DNS WP-CLI MariaDB Let's Encrypt VPS