Migrar WordPress a un nuevo servidor, sin caídas
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.
- Baja el TTL de los registros A y AAAA a 300 segundos, como mínimo 24 o 48 horas antes.
- Prepara el servidor nuevo: servidor web, PHP, base de datos, usuarios, directorios.
- Copia los archivos, copia la base de datos.
- Emite el certificado para el dominio, aunque el DNS siga apuntando al servidor antiguo.
- Ajusta el archivo hosts en tu propio equipo y recorre la web bajo el dominio real.
- Justo antes del corte, lanza una sincronización delta para que lleguen los últimos cambios.
- 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:
| Sistema | PHP | Base de datos | nginx |
|---|---|---|---|
| Debian 13 | 8.4 | MariaDB 11.8 (sin mysql-server) | 1.26.3 |
| Debian 12 | 8.2 | MariaDB 10.11 | 1.22.1 |
| Ubuntu 24.04 | 8.3 | MySQL 8.0.46 o MariaDB 10.11 | 1.24.0 |
| Ubuntu 22.04 | 8.1 | MySQL 8.0.46 o MariaDB 10.6 | 1.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:
- Activa el modo de mantenimiento en el servidor antiguo o, como mínimo, detén un momento pedidos y comentarios.
- Sincroniza los archivos otra vez, esta vez tarda segundos.
- Exporta la base de datos de nuevo e impórtala en el servidor nuevo.
- Vacía las cachés:
wp cache flushywp rewrite flush --hard. - Comprueba otra vez con curl contra la IP nueva.
- 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?
¿Por qué no puedo reemplazar el dominio en la base de datos con una simple sentencia SQL?
¿Tengo que reemplazar el dominio si lo único que cambia es el servidor?
¿Cómo consigo un certificado Let's Encrypt si el dominio todavía apunta al servidor antiguo?
He cambiado el archivo hosts, pero sigo viendo la web antigua. ¿A qué se debe?
¿Cuál es la vuelta atrás más rápida si algo no funciona después del cambio?
2026 KernelHost GmbH. Todos los derechos reservados. Esta guía está protegida por derechos de autor. Su publicación en otros sitios web, aunque sea de forma parcial o modificada, no está permitida sin nuestro consentimiento por escrito. Las citas con indicación de la fuente y un enlace son muy bienvenidas.

