La forma más segura de migrar WordPress a otro hosting sin perder datos es preparar primero el destino, crear una copia inicial, aplicar una barrera durable o sincronizar las escrituras, ejecutar un delta final, probar el nuevo servidor con el dominio forzado y cambiar DNS solo cuando exista un rollback comprobable. Después del corte, conserva el origen y vigila ambos servidores hasta que expire el periodo de observación. Esta secuencia amplía el proceso general de la documentación oficial para migrar WordPress con controles para sitios que reciben pedidos, formularios, altas o cambios de contenido.
“Sin downtime” no significa que todo sitio pueda moverse sin una interrupción perceptible. Una web principalmente informativa puede acercarse mucho; WooCommerce, Easy Digital Downloads, membresías y comunidades necesitan una breve ventana de solo lectura, sincronización específica o una arquitectura capaz de replicar escrituras. La meta honesta es downtime mínimo y ninguna escritura ignorada, no una garantía universal de cero segundos.
Elige el método antes de copiar nada
Métodos para mover WordPress a otro hosting
| Método | Conviene cuando | Ventaja | Límite que debes comprobar |
|---|---|---|---|
| Migración asistida por proveedor | El nuevo proveedor ofrece el servicio y acepta el tamaño, stack y tipo de sitio. | Reduce trabajo manual y concentra la coordinación. | Pide por escrito qué incluye: correo, DNS, prueba, ventana, datos dinámicos y rollback. No asumas que está incluido. |
| Plugin de migración | El sitio cabe en los límites de la herramienta y el origen sigue accesible. | Empaqueta archivos, base de datos y reemplazos serializados. | Puede fallar por tiempo, espacio, firewall, límites de PHP, multisite o mucho volumen de escrituras. |
| Panel a panel | Ambos alojamientos usan paneles compatibles o permiten restaurar una copia completa. | Puede trasladar cuentas, bases, correo y configuración en un mismo flujo. | Revisa versiones, propietarios, rutas, DNS, certificados y qué elementos no migra el panel. |
| Manual con SSH, rsync y WP-CLI | Administras los dos entornos y necesitas controlar cada fase. | Permite copia inicial, delta, pruebas y registros detallados. | Exige experiencia con servidor, base de datos, permisos, TLS y recuperación. |
Elige la ruta más simple que permita controlar las escrituras y volver atrás. Un plugin no elimina el problema de consistencia: si el origen acepta un pedido después de empaquetar la base, ese pedido no aparecerá mágicamente en el destino. De igual forma, una copia completa del panel no demuestra que la aplicación funcione.
Si el destino todavía no existe, revisa cómo elegir un hosting web y cómo instalar WordPress en un hosting o VPS. Confirma primero que el plan admite el tamaño, el tráfico y las versiones que necesita tu sitio.
Haz un inventario y fija criterios de go/no-go
La migración empieza con una fotografía operativa. Registra el responsable de cada comprobación y no pases al corte si falta un dato crítico:
- Aplicación: versión de WordPress, PHP y motor de base de datos; plugins, tema, mu-plugins, multisite, domain mapping, caché persistente y código personalizado.
- Capacidad: tamaño de archivos y base, inodos, espacio temporal y libre en destino, límites de subida, memoria y tiempo de ejecución.
- Servidor: document root, propietario, permisos, extensiones PHP, reglas Apache/Nginx, tareas cron, colas y procesos auxiliares.
- Integraciones: SMTP, pasarela de pago, webhooks entrantes y salientes, formularios, CRM, almacenamiento externo, búsqueda, licencias ligadas a IP o dominio y API allowlists.
- Red y nombres: zona DNS completa, A, AAAA, CNAME, MX, TXT, nameservers, proveedor autoritativo, TTL, CDN/proxy, certificado y estado DNSSEC/DS.
- Datos que cambian: pedidos, descargas, usuarios, comentarios, sesiones, reservas, leads, uploads y cualquier sistema externo que vuelva a escribir en WordPress.
Este inventario de solo lectura ofrece una base reproducible para la ruta WP-CLI. Ejecútalo como el usuario propietario del sitio, no como root, y sustituye los valores reservados:
set -eu
SOURCE_PATH=/var/www/example.com/public
SOURCE_HOST=192.0.2.10
DEST_HOST=192.0.2.20
SITE_HOST=example.com
test -d "$SOURCE_PATH"
cd "$SOURCE_PATH"
wp core version
wp option get home
wp option get siteurl
wp config get table_prefix --type=variable
wp db tables --all-tables-with-prefix --format=csv
wp plugin list --format=table
wp theme list --format=table
wp cron event list --fields=hook,next_run_gmt,next_run_relative
wp db check
wp db size --human-readable
du -sh "$SOURCE_PATH"
df -h "$SOURCE_PATH"
printf 'Origen=%s Destino=%s Sitio=%s\n' "$SOURCE_HOST" "$DEST_HOST" "$SITE_HOST"Guarda también capturas o exportaciones de la configuración que no vive en WordPress. No copies secretos a las notas. Multisite, domain mapping, clústeres, bases externas y sitios de alta escritura deben tener un runbook específico; el procedimiento genérico de esta guía no basta.
Define el go/no-go antes de la ventana: espacio libre suficiente, backup restaurable, versiones compatibles, barrera durable elegida y ensayada con bypass de operador, pruebas externas de rechazo, monitor independiente, TLS válido, checkout y formularios aprobados, logs sin errores nuevos, responsable del DNS presente y tiempo reservado para revertir. Define también un límite: si una prueba crítica no se corrige antes de una hora acordada, no se corta.
Crea un backup restaurable, no solo un archivo
Un sitio WordPress completo requiere archivos y base de datos. La guía oficial de backups de WordPress explica por qué son conjuntos distintos. Almacena la copia fuera del document root y, si es posible, fuera del servidor de origen. Una captura del panel o un snapshot puede complementar el backup, pero no sustituye una exportación portable ni una prueba de restauración.
Fija un MIGRATION_ID UTC único e inmutable para toda la corrida y no reutilices una ruta parcial. Antes de cada export o tar, test ! -e impide sobrescribir la única copia buena. Además, wp db export exporta todas las tablas de DB_NAME si omites --tables. El ejemplo obtiene con wp db tables --all-tables-with-prefix una lista CSV, la muestra para revisión y la pasa entre comillas a --tables, sin word splitting:
set -eu
MIGRATION_ID=20260814T120000Z
SOURCE_PATH=/var/www/example.com/public
BACKUP_DIR="$HOME/migration-backups/example.com-$MIGRATION_ID"
DB_BACKUP="$BACKUP_DIR/database-baseline.sql"
FILES_BACKUP="$BACKUP_DIR/files-baseline.tar.gz"
umask 077
mkdir -p "$BACKUP_DIR"
test ! -e "$DB_BACKUP"
test ! -e "$FILES_BACKUP"
cd "$SOURCE_PATH"
wp db check
SOURCE_PREFIX=$(wp config get table_prefix --type=variable)
SOURCE_TABLES=$(wp db tables --all-tables-with-prefix --format=csv)
test -n "$SOURCE_PREFIX"
test -n "$SOURCE_TABLES"
printf 'Prefijo origen: %s\nTablas propuestas:\n' "$SOURCE_PREFIX"
printf '%s\n' "$SOURCE_TABLES" | tr ',' '\n'
printf 'Confirma que la lista contiene exactamente las tablas requeridas; escribe ALCANCE_TABLAS_APROBADO: '
read -r CONFIRM
test "$CONFIRM" = ALCANCE_TABLAS_APROBADO
wp db export "$DB_BACKUP" --tables="$SOURCE_TABLES" --add-drop-table
test -s "$DB_BACKUP"
tar -czf "$FILES_BACKUP" .
test -s "$FILES_BACKUP"
tar -tzf "$FILES_BACKUP" | head
sha256sum "$DB_BACKUP" "$FILES_BACKUP"
printf 'Confirma que son artefactos nuevos, no vacíos y fuera del document root; escribe BACKUP_NUEVO_VERIFICADO: '
read -r CONFIRM
test "$CONFIRM" = BACKUP_NUEVO_VERIFICADOSi el origen comparte DB_NAME, esta lista explícita evita exportar por defecto tablas ajenas, pero debes comprobar tablas de plugins que no sigan el prefijo. Multisite, tablas compartidas o personalizadas y domain mapping requieren un runbook propio que enumere todas las dependencias; no amplíes el alcance a ciegas.
Conserva los checksums, el MIGRATION_ID, prefijo, lista aprobada, fecha, versiones y alcance. La prueba fuerte consiste en restaurar los artefactos en un entorno aislado, abrir el sitio y documentar duración y resultado. Un código de salida cero no demuestra por sí solo que el backup sea utilizable.
Prepara el destino y realiza la copia inicial
Configura en el destino el virtual host, PHP, una base de datos dedicada, usuario de base, TLS, propietarios, tareas y espacio. Reproduce las constantes necesarias de wp-config.php sin copiar credenciales a tickets. Si cambia PHP, la base o el servidor web, prueba compatibilidad antes de combinar ese cambio con el corte.
Verifica la huella SSH del destino por un canal independiente —consola del proveedor, inventario firmado o responsable autorizado— antes de aceptar la primera conexión; no confíes en una huella obtenida por la misma ruta que intentas autenticar. Si origen y destino usan Redis, Memcached u otra caché persistente, asigna al clon credenciales o namespace aislado antes de probarlo. No ejecutes un flush mientras ambos sitios compartan el mismo namespace.
Mantén desactivados en el destino cron, workers, webhooks y correo entrante/saliente hasta la activación coordinada; pagos y mensajes de prueba deben usar sandbox o destinatarios controlados. Así el clon no ejecuta trabajos duplicados antes de recibir tráfico real.
WP-CLI, rsync del document root y el dump de WordPress no migran buzones, aliases, forwarders ni historial de mensajes. Si el correo vive en el hosting anterior, crea un runbook paralelo: inventaría y provisiona cuentas/aliases/reenviadores, sincroniza mensajes con el método admitido por el servicio, valida SMTP, IMAP y entrega real, y reproduce SPF, DKIM, DMARC, PTR y allowlists donde corresponda. No cambies MX ni apagues el correo antiguo hasta completar esas pruebas. La ejecución depende de los servicios implicados; no hay un comando universal seguro.
La primera sincronización ocurre mientras el origen sigue activo. Se ejecuta como propietario del sitio, usa SSH ya verificado y no incluye --delete:
set -eu
SOURCE_PATH=/var/www/example.com/public
DEST_PATH=/var/www/example.com/public
DEST_HOST=192.0.2.20
DEST_USER=migrator
test -d "$SOURCE_PATH"
ssh "$DEST_USER@$DEST_HOST" "test -d '$DEST_PATH' && test -w '$DEST_PATH'"
rsync -aH --partial --itemize-changes \
--exclude='wp-config.php' \
--exclude='.maintenance' \
--exclude='wp-content/cache/' \
"$SOURCE_PATH/" "$DEST_USER@$DEST_HOST:$DEST_PATH/"Revisa el listado y reconcilia las constantes personalizadas. La ausencia de --delete protege archivos del destino, pero también conserva archivos eliminados en el origen. Antes de go-live compara ambos árboles y borra solo una lista aprobada. Si el equipo decide usar rsync --delete, debe tener backup del destino, las mismas exclusiones, un --dry-run --itemize-changes revisado y un gate explícito antes de la ejecución; el bloque genérico no lo hace.
La importación inicial con wp db import fija el mismo MIGRATION_ID, exporta solo la lista de tablas revisada y transfiere dump, manifest y prefijo. Antes del import compara $table_prefix con wp config get table_prefix --type=variable, muestra todas las tablas existentes en destino y exige que la base sea dedicada, vacía o expresamente aprobada:
set -eu
MIGRATION_ID=20260814T120000Z
SOURCE_PATH=/var/www/example.com/public
DEST_PATH=/var/www/example.com/public
DEST_HOST=192.0.2.20
DEST_USER=migrator
SOURCE_RUN_DIR="$HOME/migration-backups/example.com-$MIGRATION_ID"
DEST_RUN_DIR="/home/$DEST_USER/migration-backups/example.com-$MIGRATION_ID"
LOCAL_DUMP="$SOURCE_RUN_DIR/database-initial-transfer.sql"
LOCAL_TABLES="$SOURCE_RUN_DIR/database-initial.tables.csv"
LOCAL_PREFIX="$SOURCE_RUN_DIR/database-initial.prefix"
REMOTE_DUMP="$DEST_RUN_DIR/database-initial-transfer.sql"
REMOTE_TABLES="$DEST_RUN_DIR/database-initial.tables.csv"
REMOTE_PREFIX="$DEST_RUN_DIR/database-initial.prefix"
PREIMPORT_BACKUP="$DEST_RUN_DIR/before-initial-import.sql"
EXPECTED_HOME=https://example.com
EXPECTED_SITEURL=https://example.com
umask 077
mkdir -p "$SOURCE_RUN_DIR"
test ! -e "$LOCAL_DUMP"
test ! -e "$LOCAL_TABLES"
test ! -e "$LOCAL_PREFIX"
cd "$SOURCE_PATH"
wp db check
SOURCE_PREFIX=$(wp config get table_prefix --type=variable)
SOURCE_TABLES=$(wp db tables --all-tables-with-prefix --format=csv)
test -n "$SOURCE_PREFIX"
test -n "$SOURCE_TABLES"
printf 'Prefijo origen: %s\nTablas propuestas:\n' "$SOURCE_PREFIX"
printf '%s\n' "$SOURCE_TABLES" | tr ',' '\n'
printf 'Confirma el alcance exacto del sitio; escribe ALCANCE_TABLAS_APROBADO: '
read -r CONFIRM
test "$CONFIRM" = ALCANCE_TABLAS_APROBADO
printf '%s\n' "$SOURCE_TABLES" >"$LOCAL_TABLES"
printf '%s\n' "$SOURCE_PREFIX" >"$LOCAL_PREFIX"
wp db export "$LOCAL_DUMP" --tables="$SOURCE_TABLES" --add-drop-table
test -s "$LOCAL_DUMP"
test -s "$LOCAL_TABLES"
test -s "$LOCAL_PREFIX"
ssh "$DEST_USER@$DEST_HOST" bash -s -- \
"$DEST_RUN_DIR" "$REMOTE_DUMP" "$REMOTE_TABLES" "$REMOTE_PREFIX" <<'REMOTE'
set -eu
DEST_RUN_DIR=$1
REMOTE_DUMP=$2
REMOTE_TABLES=$3
REMOTE_PREFIX=$4
umask 077
mkdir -p "$DEST_RUN_DIR"
test ! -e "$REMOTE_DUMP"
test ! -e "$REMOTE_TABLES"
test ! -e "$REMOTE_PREFIX"
REMOTE
rsync -a --itemize-changes \
"$LOCAL_DUMP" "$LOCAL_TABLES" "$LOCAL_PREFIX" \
"$DEST_USER@$DEST_HOST:$DEST_RUN_DIR/"
DEST_PREFIX=$(
ssh "$DEST_USER@$DEST_HOST" bash -s -- "$DEST_PATH" <<'REMOTE'
set -eu
DEST_PATH=$1
cd "$DEST_PATH"
wp config get table_prefix --type=variable
REMOTE
)
DESTINATION_TABLES=$(
ssh "$DEST_USER@$DEST_HOST" bash -s -- "$DEST_PATH" <<'REMOTE'
set -eu
DEST_PATH=$1
cd "$DEST_PATH"
wp db tables --all-tables --format=csv
REMOTE
)
test -n "$DEST_PREFIX"
printf 'Prefijo origen=%s destino=%s\nTablas actuales destino=%s\n' \
"$SOURCE_PREFIX" "$DEST_PREFIX" "$DESTINATION_TABLES"
test "$SOURCE_PREFIX" = "$DEST_PREFIX"
printf 'Confirma que la DB destino es dedicada y vacía/aprobada; escribe DB_DESTINO_APROBADA: '
read -r CONFIRM
test "$CONFIRM" = DB_DESTINO_APROBADA
printf 'Escribe IMPORTAR_INICIAL para crear backup nuevo e importar: '
read -r CONFIRM
test "$CONFIRM" = IMPORTAR_INICIAL
ssh "$DEST_USER@$DEST_HOST" bash -s -- \
"$DEST_PATH" "$REMOTE_DUMP" "$REMOTE_TABLES" "$REMOTE_PREFIX" \
"$PREIMPORT_BACKUP" "$EXPECTED_HOME" "$EXPECTED_SITEURL" <<'REMOTE'
set -eu
DEST_PATH=$1
INCOMING_DUMP=$2
TABLE_MANIFEST=$3
PREFIX_MANIFEST=$4
PREIMPORT_BACKUP=$5
EXPECTED_HOME=$6
EXPECTED_SITEURL=$7
cd "$DEST_PATH"
umask 077
test -s "$INCOMING_DUMP"
test -s "$TABLE_MANIFEST"
test -s "$PREFIX_MANIFEST"
EXPECTED_TABLES=$(cat "$TABLE_MANIFEST")
EXPECTED_PREFIX=$(cat "$PREFIX_MANIFEST")
test -n "$EXPECTED_TABLES"
test -n "$EXPECTED_PREFIX"
test "$(wp config get table_prefix --type=variable)" = "$EXPECTED_PREFIX"
test ! -e "$PREIMPORT_BACKUP"
wp db export "$PREIMPORT_BACKUP" --add-drop-table
test -s "$PREIMPORT_BACKUP"
wp db import "$INCOMING_DUMP"
wp db check
EXPECTED_SORTED=$(printf '%s\n' "$EXPECTED_TABLES" | tr ',' '\n' | LC_ALL=C sort)
ACTUAL_SORTED=$(wp db tables --all-tables-with-prefix --format=csv | tr ',' '\n' | LC_ALL=C sort)
test "$ACTUAL_SORTED" = "$EXPECTED_SORTED"
HOME_URL=$(wp option get home)
SITE_URL=$(wp option get siteurl)
printf 'home=%s\nsiteurl=%s\n' "$HOME_URL" "$SITE_URL"
test "$HOME_URL" = "$EXPECTED_HOME"
test "$SITE_URL" = "$EXPECTED_SITEURL"
REMOTE--add-drop-table solo añade DROP TABLE para las tablas incluidas en el dump; wp db import no limpia tablas extra. Por eso el destino debe ser una DB dedicada vacía/aprobada, o tener un inventario y una limpieza separados con backup y gate. La comparación posterior exige exactamente las tablas prefijadas esperadas y valida home/siteurl. Si falla copia, backup, prefijo, alcance, import o verificación, DNS no cambia.
Usa search-replace solo cuando cambie una URL
Si dominio y protocolo finales no cambian, normalmente no necesitas reemplazar URLs. Si usaste una URL temporal, wp search-replace procesa datos serializados, a diferencia de un REPLACE() SQL indiscriminado. Excluye guid, revisa --dry-run y crea un backup nuevo, no sobrescribible, antes de aplicar:
set -eu
MIGRATION_ID=20260814T120000Z
DEST_PATH=/var/www/example.com/public
BACKUP_DIR="$HOME/migration-backups/example.com-$MIGRATION_ID"
PRE_REPLACE_BACKUP="$BACKUP_DIR/before-search-replace.sql"
OLD_URL=https://temporary.example.com
NEW_URL=https://example.com
test "$OLD_URL" != "$NEW_URL"
umask 077
mkdir -p "$BACKUP_DIR"
test ! -e "$PRE_REPLACE_BACKUP"
cd "$DEST_PATH"
wp search-replace "$OLD_URL" "$NEW_URL" \
--all-tables-with-prefix --precise --skip-columns=guid --dry-run
printf 'Revisa el dry-run y escribe APLICAR_REEMPLAZO: '
read -r CONFIRM
test "$CONFIRM" = APLICAR_REEMPLAZO
wp db export "$PRE_REPLACE_BACKUP" --add-drop-table
test -s "$PRE_REPLACE_BACKUP"
wp search-replace "$OLD_URL" "$NEW_URL" \
--all-tables-with-prefix --precise --skip-columns=guid --report-changed-only
wp db checkRevisa tablas de plugins fuera del prefijo antes de ampliar el alcance. No sustituyas cadenas que no comprendes ni cambies dominios de correo, claves o datos externos por coincidencia accidental.
Prueba el destino antes de cambiar DNS
curl --resolve conecta el nombre real a una IP elegida sin modificar DNS. Así pruebas virtual host y certificado con la URL canónica. --fail-with-body requiere curl 7.76 o posterior; en una versión anterior usa --fail y conserva el resto de controles. No añadas -k: un fallo TLS debe detener el corte.
Migra tu WordPress a un hosting listo para crecer
Prepara el destino, valida tu sitio antes del corte y conserva un plan claro de rollback.


set -eu
SITE_HOST=example.com
DEST_HOST=192.0.2.20
curl --fail-with-body --silent --show-error \
--resolve "$SITE_HOST:443:$DEST_HOST" \
--output /dev/null --write-out 'home=%{http_code}\n' \
"https://$SITE_HOST/"
curl --fail-with-body --silent --show-error \
--resolve "$SITE_HOST:443:$DEST_HOST" \
--output /dev/null --write-out 'login=%{http_code}\n' \
"https://$SITE_HOST/wp-login.php"
curl --silent --show-error --head \
--resolve "$SITE_HOST:443:$DEST_HOST" \
"https://$SITE_HOST/"Para una prueba de navegador puedes añadir temporalmente el nombre al archivo hosts de tu equipo, pero documenta y retira esa entrada después. Prueba en el destino:
- Portada, páginas, búsquedas, redirecciones, 404 y área administrativa.
- Login y cierre de sesión, roles, creación controlada de contenido y subida de una imagen desechable.
- Formulario, carrito, checkout en modo de prueba, impuestos, descarga, membresía o reserva según el negocio.
- Correo recibido —no solo “enviado”—, webhooks en ambos sentidos, cron, colas y callbacks de pago.
- HTTPS, contenido mixto, cabeceras, caché de página/objeto, CDN y purga.
- Logs PHP, web y base de datos; consumo de CPU, memoria, disco e inodos.
Comprueba también que las URLs, canonical, robots, sitemap, datos estructurados, analítica y códigos de estado coinciden con el origen. Un hosting nuevo no debería convertir una migración sin cambio de URL en un rediseño o reestructuración simultáneos.
Controla las escrituras y aplica el delta final
La copia inicial queda obsoleta en cuanto llega un pedido o upload. Enumera visitantes, administradores, WP-Cron, cron del sistema, workers, colas, webhooks, importadores y APIs; pausa cada escritor de forma reversible y registra cómo demostrarás que dejó de cambiar datos.
El modo estándar basado en .maintenance no es una barrera durable. El core, mediante wp_is_maintenance_mode(), lee el timestamp y deja de considerar activo el modo estándar tras aproximadamente diez minutos. WP-CLI puede gestionarlo como UX, pero no debe proteger pedidos o formularios durante una migración larga.
Antes de la ventana, elige y ensaya una barrera de escritura no expirable que permanezca hasta una retirada explícita en edge/CDN, webserver, aplicación o control del proveedor. Debe cubrir origen y destino; rechazar checkout, formularios, altas, comentarios, escrituras de wp-admin y REST/API; y permitir acceso de operador con bypass autenticado o allowlist controlada. No existe un comando universal seguro.
Desde una red externa sin bypass, ejecuta pruebas negativas y confirma que ninguna ruta crea registros. Mantén un monitor independiente durante delta, export, import y DNS. Si una escritura deja de rechazarse o se pierde señal, aborta, restablece la barrera, contabiliza cambios desde el último checkpoint y repite delta/import.
Este bloque exige barrera y writers pausados, reconcilia explícitamente archivos eliminados y exporta exactamente la lista de tablas revisada. El MIGRATION_ID debe coincidir con el resto de la corrida:
set -eu
MIGRATION_ID=20260814T120000Z
SOURCE_PATH=/var/www/example.com/public
SOURCE_RUN_DIR="$HOME/migration-backups/example.com-$MIGRATION_ID"
DEST_PATH=/var/www/example.com/public
DEST_HOST=192.0.2.20
DEST_USER=migrator
FINAL_DUMP="$SOURCE_RUN_DIR/database-final.sql"
FINAL_TABLES="$SOURCE_RUN_DIR/database-final.tables.csv"
FINAL_PREFIX="$SOURCE_RUN_DIR/database-final.prefix"
umask 077
mkdir -p "$SOURCE_RUN_DIR"
test ! -e "$FINAL_DUMP"
test ! -e "$FINAL_TABLES"
test ! -e "$FINAL_PREFIX"
cd "$SOURCE_PATH"
wp db check
printf 'Aplica la barrera no expirable ensayada en origen y destino; escribe BARRERA_DURABLE_VERIFICADA: '
read -r CONFIRM
test "$CONFIRM" = BARRERA_DURABLE_VERIFICADA
printf 'Tras pruebas externas de checkout, formularios, admin y APIs, escribe ESCRITURAS_EXTERNAS_BLOQUEADAS: '
read -r CONFIRM
test "$CONFIRM" = ESCRITURAS_EXTERNAS_BLOQUEADAS
printf 'Pausa cron, workers, colas, webhooks, admin y APIs; escribe TODOS_LOS_WRITERS_PAUSADOS: '
read -r CONFIRM
test "$CONFIRM" = TODOS_LOS_WRITERS_PAUSADOS
SOURCE_PREFIX=$(wp config get table_prefix --type=variable)
SOURCE_TABLES=$(wp db tables --all-tables-with-prefix --format=csv)
test -n "$SOURCE_PREFIX"
test -n "$SOURCE_TABLES"
printf 'Prefijo=%s\nTablas finales propuestas:\n' "$SOURCE_PREFIX"
printf '%s\n' "$SOURCE_TABLES" | tr ',' '\n'
printf 'Confirma el alcance exacto; escribe ALCANCE_FINAL_APROBADO: '
read -r CONFIRM
test "$CONFIRM" = ALCANCE_FINAL_APROBADO
rsync -aH --partial --itemize-changes \
--exclude='wp-config.php' \
--exclude='.maintenance' \
--exclude='wp-content/cache/' \
"$SOURCE_PATH/" "$DEST_USER@$DEST_HOST:$DEST_PATH/"
printf 'Compara árboles y resuelve solo la lista aprobada de eliminaciones; escribe ELIMINACIONES_RECONCILIADAS: '
read -r CONFIRM
test "$CONFIRM" = ELIMINACIONES_RECONCILIADAS
printf 'Confirma monitor de barrera sano; escribe BARRERA_SIGUE_ACTIVA: '
read -r CONFIRM
test "$CONFIRM" = BARRERA_SIGUE_ACTIVA
printf '%s\n' "$SOURCE_TABLES" >"$FINAL_TABLES"
printf '%s\n' "$SOURCE_PREFIX" >"$FINAL_PREFIX"
wp db export "$FINAL_DUMP" --tables="$SOURCE_TABLES" --add-drop-table
test -s "$FINAL_DUMP"
test -s "$FINAL_TABLES"
test -s "$FINAL_PREFIX"
sha256sum "$FINAL_DUMP"
printf 'Confirma dump y manifests nuevos/no vacíos; escribe ARTEFACTOS_FINALES_VERIFICADOS: '
read -r CONFIRM
test "$CONFIRM" = ARTEFACTOS_FINALES_VERIFICADOSTransfiere dump y manifests. Compara SHA-256 antes de cualquier gate de import; compara prefijos, inventaría todas las tablas del destino y exige su aprobación. El flush de caché solo queda autorizado tras confirmar un namespace aislado:
set -eu
MIGRATION_ID=20260814T120000Z
SOURCE_PATH=/var/www/example.com/public
DEST_PATH=/var/www/example.com/public
DEST_HOST=192.0.2.20
DEST_USER=migrator
SOURCE_RUN_DIR="$HOME/migration-backups/example.com-$MIGRATION_ID"
DEST_RUN_DIR="/home/$DEST_USER/migration-backups/example.com-$MIGRATION_ID"
LOCAL_DUMP="$SOURCE_RUN_DIR/database-final.sql"
LOCAL_TABLES="$SOURCE_RUN_DIR/database-final.tables.csv"
LOCAL_PREFIX="$SOURCE_RUN_DIR/database-final.prefix"
REMOTE_DUMP="$DEST_RUN_DIR/database-final.sql"
REMOTE_TABLES="$DEST_RUN_DIR/database-final.tables.csv"
REMOTE_PREFIX="$DEST_RUN_DIR/database-final.prefix"
PREIMPORT_BACKUP="$DEST_RUN_DIR/before-final-import.sql"
EXPECTED_HOME=https://example.com
EXPECTED_SITEURL=https://example.com
test -s "$LOCAL_DUMP"
test -s "$LOCAL_TABLES"
test -s "$LOCAL_PREFIX"
ssh "$DEST_USER@$DEST_HOST" bash -s -- \
"$DEST_RUN_DIR" "$REMOTE_DUMP" "$REMOTE_TABLES" "$REMOTE_PREFIX" <<'REMOTE'
set -eu
DEST_RUN_DIR=$1
REMOTE_DUMP=$2
REMOTE_TABLES=$3
REMOTE_PREFIX=$4
umask 077
mkdir -p "$DEST_RUN_DIR"
test ! -e "$REMOTE_DUMP"
test ! -e "$REMOTE_TABLES"
test ! -e "$REMOTE_PREFIX"
REMOTE
rsync -a --itemize-changes \
"$LOCAL_DUMP" "$LOCAL_TABLES" "$LOCAL_PREFIX" \
"$DEST_USER@$DEST_HOST:$DEST_RUN_DIR/"
LOCAL_SHA256=$(sha256sum "$LOCAL_DUMP" | awk '{print $1}')
REMOTE_SHA256=$(
ssh "$DEST_USER@$DEST_HOST" bash -s -- "$REMOTE_DUMP" <<'REMOTE'
set -eu
REMOTE_DUMP=$1
test -s "$REMOTE_DUMP"
sha256sum "$REMOTE_DUMP" | awk '{print $1}'
REMOTE
)
test -n "$LOCAL_SHA256"
test -n "$REMOTE_SHA256"
printf 'local=%s remoto=%s\n' "$LOCAL_SHA256" "$REMOTE_SHA256"
test "$LOCAL_SHA256" = "$REMOTE_SHA256"
cd "$SOURCE_PATH"
SOURCE_PREFIX=$(cat "$LOCAL_PREFIX")
DEST_PREFIX=$(
ssh "$DEST_USER@$DEST_HOST" bash -s -- "$DEST_PATH" <<'REMOTE'
set -eu
DEST_PATH=$1
cd "$DEST_PATH"
wp config get table_prefix --type=variable
REMOTE
)
DESTINATION_TABLES=$(
ssh "$DEST_USER@$DEST_HOST" bash -s -- "$DEST_PATH" <<'REMOTE'
set -eu
DEST_PATH=$1
cd "$DEST_PATH"
wp db tables --all-tables --format=csv
REMOTE
)
test -n "$SOURCE_PREFIX"
test -n "$DEST_PREFIX"
printf 'Prefijo origen=%s destino=%s\nTablas actuales destino=%s\n' \
"$SOURCE_PREFIX" "$DEST_PREFIX" "$DESTINATION_TABLES"
test "$SOURCE_PREFIX" = "$DEST_PREFIX"
printf 'Confirma DB destino dedicada y contenido aprobado; escribe DB_DESTINO_APROBADA: '
read -r CONFIRM
test "$CONFIRM" = DB_DESTINO_APROBADA
printf 'Confirma monitor de barrera sano; escribe BARRERA_SIGUE_ACTIVA: '
read -r CONFIRM
test "$CONFIRM" = BARRERA_SIGUE_ACTIVA
printf 'Confirma namespace de caché destino aislado; escribe CACHE_DESTINO_AISLADA: '
read -r CONFIRM
test "$CONFIRM" = CACHE_DESTINO_AISLADA
printf 'Confirma writers pausados; escribe IMPORTAR_FINAL: '
read -r CONFIRM
test "$CONFIRM" = IMPORTAR_FINAL
ssh "$DEST_USER@$DEST_HOST" bash -s -- \
"$DEST_PATH" "$REMOTE_DUMP" "$REMOTE_TABLES" "$REMOTE_PREFIX" \
"$PREIMPORT_BACKUP" "$EXPECTED_HOME" "$EXPECTED_SITEURL" <<'REMOTE'
set -eu
DEST_PATH=$1
INCOMING_DUMP=$2
TABLE_MANIFEST=$3
PREFIX_MANIFEST=$4
PREIMPORT_BACKUP=$5
EXPECTED_HOME=$6
EXPECTED_SITEURL=$7
cd "$DEST_PATH"
umask 077
test -s "$INCOMING_DUMP"
test -s "$TABLE_MANIFEST"
test -s "$PREFIX_MANIFEST"
EXPECTED_TABLES=$(cat "$TABLE_MANIFEST")
EXPECTED_PREFIX=$(cat "$PREFIX_MANIFEST")
test -n "$EXPECTED_TABLES"
test -n "$EXPECTED_PREFIX"
test "$(wp config get table_prefix --type=variable)" = "$EXPECTED_PREFIX"
test ! -e "$PREIMPORT_BACKUP"
wp db export "$PREIMPORT_BACKUP" --add-drop-table
test -s "$PREIMPORT_BACKUP"
wp db import "$INCOMING_DUMP"
wp db check
EXPECTED_SORTED=$(printf '%s\n' "$EXPECTED_TABLES" | tr ',' '\n' | LC_ALL=C sort)
ACTUAL_SORTED=$(wp db tables --all-tables-with-prefix --format=csv | tr ',' '\n' | LC_ALL=C sort)
test "$ACTUAL_SORTED" = "$EXPECTED_SORTED"
HOME_URL=$(wp option get home)
SITE_URL=$(wp option get siteurl)
printf 'home=%s\nsiteurl=%s\n' "$HOME_URL" "$SITE_URL"
test "$HOME_URL" = "$EXPECTED_HOME"
test "$SITE_URL" = "$EXPECTED_SITEURL"
wp cache flush
REMOTE
printf 'Confirma que no se aceptaron escrituras y la barrera resistió el import; escribe SIN_ESCRITURAS_DURANTE_IMPORT: '
read -r CONFIRM
test "$CONFIRM" = SIN_ESCRITURAS_DURANTE_IMPORT--add-drop-table no elimina tablas extra: solo sustituye las incluidas. No cambies DNS si checksum, backup nuevo, prefijo, inventario de tablas, URLs, caché aislada, reconciliación de archivos eliminados o monitor de barrera fallan. Repite las pruebas críticas con bypass controlado; la barrera durable sigue cubriendo ambos servidores.
Reduce el riesgo del corte DNS
Baja el TTL antes de la migración y espera al menos el TTL anterior para que el cambio tenga efecto en cachés nuevas. La guía de preparación DNS de Cloudflare describe este principio. Un TTL bajo acorta la vigencia de respuestas futuras; no borra respuestas ya almacenadas ni garantiza un instante de cambio global.
Si conservas el mismo DNS autoritativo, normalmente basta con cambiar A y, si existe, AAAA. Un AAAA antiguo puede enviar usuarios IPv6 al servidor anterior aunque A ya apunte al nuevo. Cambiar nameservers implica reproducir toda la zona —incluidos MX, TXT y validaciones— y coordinar DNSSEC: un registro DS en el registrador que no corresponda con las claves de la nueva zona puede producir SERVFAIL. Consulta cómo funcionan DNS, registros y TTL antes de cambiar delegación.
El corte es go solo si: backup y rollback están disponibles; la barrera durable cubre origen y destino y su monitor está sano; todos los writers están pausados; checksum e import son válidos; destino, TLS e integraciones críticas están aprobados; A y AAAA están definidos; TTL preparado; DNSSEC y correo inventariados; y responsables presentes. Cambia únicamente los registros previstos, guarda su valor anterior y anota la hora.
set -eu
SITE_HOST=example.com
SOURCE_HOST=192.0.2.10
DEST_HOST=192.0.2.20
printf 'Esperado origen=%s destino=%s\n' "$SOURCE_HOST" "$DEST_HOST"
dig +short A "$SITE_HOST"
dig +short AAAA "$SITE_HOST"
curl --silent --show-error --output /dev/null \
--write-out 'source=%{http_code}\n' \
--resolve "$SITE_HOST:443:$SOURCE_HOST" "https://$SITE_HOST/"
curl --silent --show-error --output /dev/null \
--write-out 'destination=%{http_code}\n' \
--resolve "$SITE_HOST:443:$DEST_HOST" "https://$SITE_HOST/"Haz las consultas desde varios resolvers y redes. El resultado de tu equipo no representa a todos los visitantes. Mantén la barrera en ambos servidores mientras las cachés mezclan respuestas; una solicitud que alcance el origen no debe poder crear una rama de datos.
Activa los procesos del destino exactamente una vez
Cuando DNS y el tráfico observado ya correspondan al destino, mantén todavía cerradas las escrituras públicas. Con el runbook ensayado del stack, activa una sola instancia de cron, workers, consumidores de colas, webhooks y correo entrante/saliente en el destino. Comprueba heartbeat del scheduler, propietario del lock, consumo de una cola de prueba, un webhook idempotente y entrega controlada de correo; confirma a la vez que esos procesos siguen detenidos en el origen.
Solo después de esa verificación retira la barrera del destino mediante el control aprobado, conserva la del origen y ejecuta un canary transaccional trazable. Este bloque coordina gates; no pretende conocer los comandos de tu proveedor, supervisor o aplicación:
set -eu
DEST_PATH=/var/www/example.com/public
DEST_HOST=192.0.2.20
SITE_HOST=example.com
cd "$DEST_PATH"
wp db check
printf 'Confirma barrera durable en ambos hosts y writers del origen pausados; escribe BARRERA_CORTE_ACTIVA: '
read -r CONFIRM
test "$CONFIRM" = BARRERA_CORTE_ACTIVA
printf 'Usa el runbook ensayado para activar una instancia destino de cron, workers, colas, webhooks y correo entrante/saliente; escribe WRITERS_DESTINO_UNA_INSTANCIA: '
read -r CONFIRM
test "$CONFIRM" = WRITERS_DESTINO_UNA_INSTANCIA
printf 'Verifica heartbeats, locks, colas, un webhook y correo sin duplicados; escribe WRITERS_DESTINO_VERIFICADOS: '
read -r CONFIRM
test "$CONFIRM" = WRITERS_DESTINO_VERIFICADOS
printf 'Desde fuera, confirma que las rutas de escritura siguen bloqueadas; escribe PRUEBAS_NEGATIVAS_CORRECTAS: '
read -r CONFIRM
test "$CONFIRM" = PRUEBAS_NEGATIVAS_CORRECTAS
printf 'Retira solo la barrera del destino y conserva el origen bloqueado; escribe ESCRITURAS_DESTINO_ABIERTAS: '
read -r CONFIRM
test "$CONFIRM" = ESCRITURAS_DESTINO_ABIERTAS
curl --fail-with-body --silent --show-error \
--resolve "$SITE_HOST:443:$DEST_HOST" \
--output /dev/null "https://$SITE_HOST/"
printf 'Completa un canary de escritura trazable y verifica un único resultado; escribe CORTE_DESTINO_VERIFICADO: '
read -r CONFIRM
test "$CONFIRM" = CORTE_DESTINO_VERIFICADOMonitoriza la migración y valida SEO
Durante el periodo de observación, compara tráfico y logs del origen y destino, tasa de 5xx, latencia, errores PHP, conexiones de base, colas, cron, webhooks, correos, pedidos y formularios. Conserva una forma inequívoca de saber qué servidor respondió, por ejemplo mediante logs o una cabecera interna no cacheada; no muestres detalles sensibles al público.
La guía de Google para cambiar de hosting sin cambiar URLs recomienda preparar y probar el nuevo alojamiento, actualizar DNS y monitorizar el tráfico. Comprueba que las URLs mantienen sus códigos, canonical, robots y sitemap, que no apareció un noindex del staging y que CDN/caché no sirven errores. No uses la herramienta de cambio de dirección si el dominio no cambió.
Después de estabilizar, mide rendimiento y capacidad contra la línea base. Si hay regresiones, sigue una revisión ordenada como estas comprobaciones para acelerar un WordPress lento. Evita activar varias optimizaciones durante el corte: dificultan atribuir un fallo y volver atrás.
Define un rollback que preserve las escrituras
Activa rollback por criterios acordados: checkout/login inoperable, 5xx sostenidos, corrupción o conexión de base, TLS inválido, integraciones críticas caídas o una desviación de datos que no pueda corregirse dentro del límite.
Siempre, incluso si crees que el destino no recibió escrituras, aplica o confirma primero su barrera durable y pausa todos sus writers antes de evaluar logs, capturar delta o revertir DNS. Conserva el destino cerrado para clientes con DNS cacheado. Si sí recibió pedidos, usuarios o formularios, captura y reconcilia ese delta con un procedimiento probado antes de devolver DNS; apuntar atrás sin reconciliar perdería datos.
Recoge evidencia fechada del bloqueo del destino, writers detenidos, rango de IDs/logs revisado, reconciliación y DNS restaurado. SOURCE_DATA_SAFE solo es válido cuando esos artefactos existen y fueron revisados. Con la barrera del origen aún activa, valida base y aplicación por bypass; reactiva exactamente una instancia de cron, workers, colas, webhooks y correo entrante/saliente en origen, comprueba cero en destino y solo entonces abre el origen. Mantén el destino bloqueado hasta cerrar la incidencia.
set -eu
MIGRATION_ID=20260814T120000Z
SOURCE_PATH=/var/www/example.com/public
SOURCE_HOST=192.0.2.10
SITE_HOST=example.com
EVIDENCE_DIR="$HOME/migration-evidence/example.com-$MIGRATION_ID"
DEST_FREEZE_EVIDENCE="$EVIDENCE_DIR/destination-barrier-and-writers.txt"
RECONCILIATION_EVIDENCE="$EVIDENCE_DIR/reconciliation-report.txt"
DNS_EVIDENCE="$EVIDENCE_DIR/dns-restored-to-source.txt"
cd "$SOURCE_PATH"
wp db check
printf 'Aplica barrera durable al destino y pausa sus writers; escribe DESTINO_CONGELADO: '
read -r CONFIRM
test "$CONFIRM" = DESTINO_CONGELADO
test -s "$DEST_FREEZE_EVIDENCE"
test -s "$RECONCILIATION_EVIDENCE"
test -s "$DNS_EVIDENCE"
printf 'Tras revisar evidencia de freeze, reconciliación y DNS, escribe SOURCE_DATA_SAFE: '
read -r CONFIRM
test "$CONFIRM" = SOURCE_DATA_SAFE
printf 'Confirma que la barrera del origen bloquea escrituras externas; escribe BARRERA_ORIGEN_ACTIVA: '
read -r CONFIRM
test "$CONFIRM" = BARRERA_ORIGEN_ACTIVA
printf 'Valida origen mediante acceso de operador; escribe ORIGEN_VALIDADO_POR_BYPASS: '
read -r CONFIRM
test "$CONFIRM" = ORIGEN_VALIDADO_POR_BYPASS
printf 'Activa exactamente una instancia de cron, workers, colas, webhooks y correo entrante/saliente en origen; escribe WRITERS_ORIGEN_UNA_INSTANCIA: '
read -r CONFIRM
test "$CONFIRM" = WRITERS_ORIGEN_UNA_INSTANCIA
printf 'Verifica writers origen=1 y destino=0; escribe WRITERS_ROLLBACK_VERIFICADOS: '
read -r CONFIRM
test "$CONFIRM" = WRITERS_ROLLBACK_VERIFICADOS
printf 'Retira barrera del origen y conserva destino bloqueado; escribe ESCRITURAS_ORIGEN_ABIERTAS: '
read -r CONFIRM
test "$CONFIRM" = ESCRITURAS_ORIGEN_ABIERTAS
curl --fail-with-body --silent --show-error \
--resolve "$SITE_HOST:443:$SOURCE_HOST" \
--output /dev/null "https://$SITE_HOST/"
printf 'Completa un canary de escritura trazable en origen; escribe ROLLBACK_ORIGEN_VERIFICADO: '
read -r CONFIRM
test "$CONFIRM" = ROLLBACK_ORIGEN_VERIFICADODocumenta quién autorizó la vuelta, datos reconciliados, evidencia, canaries y clientes que pudieron ver cada origen. Mantén backups, logs y destino fallido para análisis; no sobrescribas evidencia.
Troubleshooting después de mover WordPress
Diagnóstico de fallos frecuentes tras una migración
| Síntoma | Qué comprobar primero | Acción segura |
|---|---|---|
| Error de conexión a la base | Nombre, usuario, host, privilegios, socket/puerto y valores efectivos de wp-config.php. | Compara con la base dedicada y prueba wp db check; no pongas la contraseña en el comando. |
| Error 500 o pantalla en blanco | Logs privados, versión/extensiones PHP, memoria, mu-plugins y último cambio. | Revierte un cambio cada vez o restaura el conjunto conocido; no habilites errores sensibles en producción. |
| Bucle de redirección | home, siteurl, HTTPS del proxy/CDN y reglas duplicadas. | Define una sola capa responsable de la redirección y prueba origen/destino por separado. |
| Contenido mixto | HTML, CSS, widgets y opciones con URL HTTP. | Localiza el dato y usa un search-replace serializado con dry-run; no ocultes el problema desactivando TLS. |
| 404 en páginas internas | Enlaces permanentes, .htaccess o reglas Nginx y document root. | Regenera reglas desde WordPress o aplica la configuración del servidor previamente respaldada. |
| Uploads o actualizaciones sin permiso | Propietario real de PHP, grupo, ACL y rutas de escritura. | Restaura propietarios/permisos del modelo del hosting; nunca uses chmod 777. |
| Certificado inválido | SAN para dominio/www, cadena, SNI, proxy/CDN y A/AAAA. | Corrige emisión y routing; no uses curl -k ni desactives validación. |
| Correo o webhooks no llegan | SMTP, DNS de correo, allowlists de IP, secretos, URLs de callback, cron y logs del tercero. | Envía una prueba trazable y reintenta eventos idempotentes; no dupliques cargos o mensajes. |
| Se ve contenido antiguo | DNS, CDN, caché de página/objeto/navegador y qué IP respondió. | Identifica la capa antes de purgar; valida directamente cada origen con --resolve. |
| La barrera deja aceptar una escritura | Prueba externa de checkout/form/admin/API, salud del monitor y alcance real en origen y destino. | Aborta el corte, restablece la barrera, contabiliza cambios desde el último checkpoint y repite delta/import; no abras el destino. |
Cuándo retirar el hosting anterior
No elimines el origen cuando veas la primera respuesta correcta del destino. Espera a que venza el periodo de observación definido según el TTL anterior, el ciclo de negocio y las integraciones demoradas. Confirma que el origen ya no recibe tráfico útil ni webhooks, que los backups nuevos se completan, que una restauración del destino está programada o probada y que métricas y SEO permanecen estables.
Después, revoca accesos temporales, elimina entradas del archivo hosts, actualiza allowlists y documentación, reanuda cron/colas en un solo sitio, restaura el TTL operativo mediante un cambio controlado y verificado, y conserva respaldos según la política. Retira el origen únicamente con autorización y evidencia de que no contiene la única copia de un dato reciente.
Preguntas frecuentes
¿Cuánto tarda migrar WordPress a otro hosting?
Depende del tamaño, velocidad entre servidores, prueba y ventana de escritura. La copia inicial puede ejecutarse sin interrumpir el sitio; el tiempo sensible es el delta, la validación y el corte. Mídelo con un ensayo, no con una promesa genérica.
¿Un plugin evita perder pedidos?
No por sí solo. El paquete refleja un momento. Debes impedir o sincronizar los pedidos creados después, verificar el último identificador y probar pagos, webhooks y correo antes de abrir el destino.
¿Tengo que cambiar los nameservers?
No si mantienes el proveedor DNS actual: suele bastar A/AAAA. Cambiar nameservers amplía el riesgo porque debes reconstruir la zona, correo y DNSSEC.
¿Puedo migrar un multisite con este procedimiento?
Úsalo solo como marco. Multisite, subdominios, domain mapping, redes grandes y tablas personalizadas necesitan inventario, reemplazos y pruebas específicos. Escala la ejecución a un runbook propio.
Una migración segura termina después del corte
Migrar WordPress sin perder datos consiste en controlar el estado, no solo en copiar carpetas. Elige el método, inventaría los escritores, prueba un restore, prepara el destino, ejecuta copia inicial y delta bajo una ventana controlada, valida por hostname, corta DNS y observa. Mantén el origen hasta demostrar que la web, las integraciones, los backups y el SEO funcionan en el nuevo hosting.








