Migrare WordPress su un nuovo server, senza downtime
L'ordine decide tutto: prepara il nuovo server, copia i dati, testa con il file hosts sotto il dominio reale, emetti il certificato in anticipo e solo dopo cambia il DNS. Con la via di ritorno e i messaggi di errore reali.
Una migrazione WordPress raramente fallisce per colpa di un singolo comando. Fallisce per colpa dell'ordine delle operazioni. Se cambi prima il DNS e copi dopo, ti garantisci una finestra in cui i visitatori vedono una pagina di errore oppure un'installazione riempita a metà. Procedendo al contrario puoi completare con calma il nuovo server, provarlo sotto il dominio reale e modificare il record DNS solo nel momento in cui tutto funziona in modo verificabile.
Questo articolo descrive esattamente quest'ordine, insieme ai punti in cui nella pratica salta tutto: i dati serializzati nel database, le collation nel passaggio da MySQL a MariaDB, i certificati senza un record DNS che punti al server giusto e la via di ritorno, nel caso qualcosa sia comunque sfuggito.
La sequenza che non produce downtime
Il vecchio sito resta online fino all'ultimo momento. Non si spegne nulla, non si cancella nulla. Il nuovo server gira in parallelo, viene testato e subentra soltanto quando il record DNS è stato cambiato.
- Abbassa il TTL dei record A e AAAA a 300 secondi, almeno 24 o 48 ore prima.
- Prepara il nuovo server: web server, PHP, database, utenti, directory.
- Copia i file, copia il database.
- Emetti il certificato per il dominio, anche se il DNS punta ancora al vecchio server.
- Adatta il file hosts sul tuo computer e naviga il sito sotto il dominio reale.
- Poco prima dello switch lancia una sincronizzazione differenziale, così arrivano anche le ultime modifiche.
- Cambia il DNS. Lascia il vecchio server acceso ancora per diversi giorni.
L'unico punto in cui teoricamente si apre una finestra di indisponibilità è il passo 6. Quanto sarà breve lo decide il passo 1.
Abbassare il TTL prima di qualsiasi altra cosa
Il TTL dice ai resolver di tutto il mondo per quanto tempo possono tenere una risposta in cache. Se è impostato su 86400, un resolver può continuare a servire il tuo vecchio IP per altre 24 ore dopo il cambio. E c'è un dettaglio che sfugge volentieri: un TTL abbassato ha effetto solo dopo la scadenza di quello vecchio. Se passi da 86400 a 300, può volerci un giorno intero prima che tutti i resolver conoscano il valore breve.
Per questo è la prima operazione, non l'ultima. dig non fa parte della dotazione di base di nessuna distribuzione e in questa guida serve continuamente, quindi installalo per primo:
apt install -y bind9-dnsutils
Su Debian 11, Ubuntu 22.04 e Ubuntu 24.04 il pacchetto si chiama ancora dnsutils. Da Debian 12 in poi dnsutils è soltanto un pacchetto di transizione che rimanda a bind9-dnsutils, su Debian 13 è addirittura un pacchetto puramente virtuale, senza una versione propria. Funzionano entrambi, perché apt risolve da solo l'unico fornitore, ma bind9-dnsutils è il nome destinato a restare. Dopodiché verifica il valore attuale:
dig +noall +answer example.com A
dig +noall +answer example.com SOA
Il numero nella seconda colonna della risposta A è il TTL residuo. Interroga direttamente il server autoritativo, altrimenti vedi solo il valore rimasto nella cache del tuo resolver:
dig @a.ns14.net example.com A +noall +answer
Dopo la migrazione rialza il TTL a 3600 o più. TTL brevi in modo permanente generano carico inutile e allungano i disservizi del tuo provider DNS.
Allestire il nuovo server in modo coerente
Qui nasce la maggior parte delle brutte sorprese, perché la distribuzione sul nuovo server porta versioni diverse rispetto alla vecchia. Tutti i comandi di pacchetto di questa guida presuppongono Debian o Ubuntu: su AlmaLinux, Rocky Linux e Oracle Linux non esiste apt e i nomi dei pacchetti sono diversi. A luglio 2026 la situazione è questa:
| Sistema | PHP | Database | nginx |
|---|---|---|---|
| Debian 13 | 8.4 | MariaDB 11.8 (nessun mysql-server) | 1.26.3 |
| Debian 12 | 8.2 | MariaDB 10.11 | 1.22.1 |
| Ubuntu 24.04 | 8.3 | MySQL 8.0.46 oppure MariaDB 10.11 | 1.24.0 |
| Ubuntu 22.04 | 8.1 | MySQL 8.0.46 oppure MariaDB 10.6 | 1.18.0 |
Da qui derivano due conseguenze. Primo: su Debian non ottieni mysql-server, lì gira sempre MariaDB. Se il vecchio sito girava su MySQL 8, la migrazione verso Debian è allo stesso tempo un cambio di database, con una trappola molto concreta descritta più avanti. Secondo: il salto da PHP 8.1 a 8.4 non è automatico. Temi e plugin datati reagiscono alle funzioni rimosse con un Fatal error: Uncaught Error: Call to undefined function oppure con una pagina bianca. Se la vecchia installazione girava su PHP 8.1 e non vuoi accoppiare la migrazione a un aggiornamento di PHP, Ubuntu 22.04 o una versione di PHP da un repository di terze parti è la scelta più tranquilla. Quello che nella tabella manca di proposito è Debian 11: lì php-cli porta ancora PHP 7.4.33, quindi una versione senza manutenzione di sicurezza e sotto quanto WordPress raccomanda. Come destinazione di una migrazione Debian 11 è così da escludere, a meno che tu non integri prima il repository Sury.
I pacchetti di base per un'installazione tipica con nginx e 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
I dettagli sul web server si trovano nella guida a nginx, la messa in sicurezza di base del database in Mettere in sicurezza MariaDB e MySQL. Se il server è appena stato consegnato, vale la pena dare prima un'occhiata alla checklist per i nuovi server root.
Crea database e utente, con lo stesso set di caratteri della sorgente:
CREATE DATABASE wp_nuovo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_nuovo'@'localhost' IDENTIFIED BY 'una-password-lunga';
GRANT ALL PRIVILEGES ON wp_nuovo.* TO 'wp_nuovo'@'localhost';
FLUSH PRIVILEGES;
Trasferire i file senza rovinare i permessi
La copia avviene direttamente da server a server, non passando dalla tua connessione di casa. Il modo più semplice è rsync dal vecchio al nuovo server, con una chiave SSH al posto della password:
rsync -az --delete --exclude 'wp-content/cache/' --exclude 'wp-content/uploads/backup*' \
-e 'ssh -p 22' /var/www/html/ root@nuovo.ip:/var/www/html/
La prima passata può durare a lungo. È esattamente per questo che la si lancia giorni prima dello switch e poco prima si ripete soltanto lo stesso comando, che a quel punto trasferisce solo le differenze.
Dopo la copia vanno rimessi a posto proprietario e permessi, perché gli UID cambiano da sistema a sistema e rsync trasferisce numeri, non nomi:
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
Prima del primo avvio volano fuori due file, se presenti: wp-content/object-cache.php e wp-content/advanced-cache.php. Sono entrambi drop-in di plugin di cache che puntano a un socket Redis o Memcached che sul nuovo server ancora non esiste. Il risultato sarebbe una pagina bianca senza alcun messaggio utile.
Trasferire il database e la trappola delle collation
Il dump si fa con WP-CLI oppure in modo classico. WP-CLI ha il vantaggio di prendere set di caratteri e prefisso dal file wp-config.php:
cd /var/www/html
wp db export /root/wp-dump.sql --add-drop-table
Senza WP-CLI, su un sistema MariaDB recente lo strumento si chiama mariadb-dump. Il vecchio nome mysqldump da MariaDB 11.0 in poi è soltanto un symlink deprecato e ti accoglie con Deprecated program name. It will be removed in a future release:
mariadb-dump --single-transaction --default-character-set=utf8mb4 wp_vecchio > /root/wp-dump.sql
Ed ecco il punto in cui una migrazione da Ubuntu con MySQL 8 verso Debian con MariaDB cade regolarmente. MySQL 8 usa come impostazione predefinita la collation utf8mb4_0900_ai_ci, che in MariaDB non esiste affatto. L'import si interrompe con:
ERROR 1273 (HY000) at line 42: Unknown collation: 'utf8mb4_0900_ai_ci'
Riparazione prima dell'import, direttamente nel 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
Poi importa:
mariadb --default-character-set=utf8mb4 wp_nuovo < /root/wp-dump.sql
Se dopo l'import leggi ovunque perché al posto di perché, il set di caratteri è andato perso durante il dump oppure durante l'import. Non si ripara con un cerca e sostituisci, ma ripetendo dump e import con --default-character-set=utf8mb4. È proprio per questo che a questo punto il vecchio database è ancora intatto.
Adatta poi le credenziali nel file wp-config.php sul nuovo server: DB_NAME, DB_USER, DB_PASSWORD e soprattutto DB_HOST, se la vecchia installazione puntava a un server di database remoto. Se lì resta il vecchio indirizzo, vedi Error establishing a database connection anche se in locale funziona tutto. Se la connessione si blocca nonostante le credenziali corrette, ti aiuta Risolvere Access denied for user.
Sostituire il dominio, e perché un REPLACE in SQL distrugge il sito
Prima la buona notizia: se cambia solo il server e il dominio resta lo stesso, nel database non devi sostituire proprio nulla. È esattamente uno dei motivi per cui il test tramite il file hosts è superiore al test tramite un dominio provvisorio. La sostituzione diventa necessaria solo se il dominio cambia davvero, se colleghi la migrazione al passaggio da http a https oppure se lavori comunque con un dominio di prova e devi poi fare la sostituzione inversa.
E adesso il motivo per cui questa operazione non si fa mai con un semplice comando SQL. WordPress salva opzioni, impostazioni dei widget e configurazioni dei temi come array PHP serializzati. Un URL lì dentro non compare nudo, ma preceduto dalla sua lunghezza in byte:
s:19:"https://example.com"
Se sostituisci example.com con nuovo-dominio.it tramite UPDATE ... REPLACE(), la stringa si allunga ma il numero 19 resta dov'è. Dopo di che PHP non riesce più a decodificare l'array, unserialize() restituisce false e l'impostazione interessata è persa. Sintomi tipici: widget spariti, opzioni del tema azzerate, voci di menu vuote e nei log Notice: unserialize(): Error at offset.
WP-CLI risolve la cosa correttamente, perché deserializza i dati, esegue la sostituzione e li riscrive con l'indicazione di lunghezza corretta. Sempre prima con --dry-run:
wp search-replace 'https://vecchio-dominio.it' 'https://nuovo-dominio.it' --all-tables --precise --dry-run --report-changed-only
Che cosa fanno le singole opzioni: --all-tables tocca anche le tabelle che non seguono il prefisso di WordPress, per esempio quelle di plugin per negozi o moduli. --precise forza l'elaborazione in PHP invece che in SQL, è più lento ma gestisce i dati serializzati in modo affidabile. --report-changed-only riduce l'output alle tabelle con corrispondenze reali.
Se l'anteprima sembra plausibile, lancia lo stesso comando senza --dry-run. Poi arriva la passata che quasi tutte le guide dimenticano: i page builder come Elementor salvano i loro contenuti come JSON nel database, e lì le barre oblique sono in forma escape. Una sostituzione di https://vecchio-dominio.it semplicemente non trova https:\/\/vecchio-dominio.it. Da qui una seconda passata:
wp search-replace 'https:\/\/vecchio-dominio.it' 'https:\/\/nuovo-dominio.it' --all-tables --precise --report-changed-only
Con Elementor fa parte della procedura anche rigenerare i file CSS nel backend, sotto Strumenti, altrimenti i fogli di stile generati continuano a puntare al vecchio dominio.
Due cose che search-replace non raggiunge: le costanti nel file wp-config.php e il multisito. Se lì trovi define('WP_HOME', 'https://vecchio-dominio.it');, questo sovrascrive qualsiasi valore del database e tu cerchi per ore nel posto sbagliato. Con il multisito ti serve in più --network e devi adattare i domini in wp_blogs e wp_site.
E un avvertimento sul prefisso delle tabelle: non cambiarlo durante la migrazione. Il prefisso compare anche nel nome dell'opzione wp_user_roles e nei campi meta utente wp_capabilities e wp_user_level. Chi rinomina soltanto le tabelle riesce ancora ad accedere, ma subito dopo ottiene Sorry, you are not allowed to access this page. e non ha più un amministratore.
Emettere il certificato prima che il DNS punti al nuovo server
Qui si nasconde un problema dell'uovo e della gallina. La consueta validazione HTTP di Let's Encrypt richiede che il nome di dominio punti già al server da validare. In questa fase però non è così, perché deve ancora servire il vecchio sito. Se lanci comunque certbot --nginx, ottieni:
Certbot failed to authenticate some domains (authenticator: nginx).
Invalid response from http://example.com/.well-known/acme-challenge/...: 404
La via d'uscita pulita è la validazione DNS. Controlla un record TXT _acme-challenge.example.com e non le interessa dove punti il record A:
certbot certonly --manual --preferred-challenges dns -d example.com -d www.example.com
Certbot mostra un valore che devi inserire come record TXT nella tua zona DNS. Prima di confermare, verifica assolutamente di persona che la zona serva già il record, altrimenti bruci un tentativo fallito:
dig +short TXT _acme-challenge.example.com
Per i domini wildcard oppure se vuoi automatizzare la cosa, la strada tramite l'API DNS è descritta nell'articolo sul certificato wildcard. La variante manuale non si rinnova da sola, quindi dopo lo switch DNS passa alla normale validazione HTTP. Da quel momento funziona, perché il dominio punta ora al nuovo server.
Testare con il file hosts, come un visitatore reale
Ora arriva la parte che fa la differenza tra "dovrebbe funzionare" e "funziona". Devii verso il nuovo server soltanto il tuo computer, mentre il resto del mondo continua a vedere il vecchio sito.
Su Linux e macOS modifichi /etc/hosts come root, su Windows C:\Windows\System32\drivers\etc\hosts in un editor avviato come amministratore. Aggiungi una riga con l'IP del nuovo server:
203.0.113.10 example.com www.example.com
Poi svuota la cache DNS, altrimenti la modifica non ha effetto subito:
resolvectl flush-caches
Su Windows ipconfig /flushdns, su macOS sudo dscacheutil -flushcache seguito da sudo killall -HUP mDNSResponder. Verifica che abbia funzionato:
getent hosts example.com
Una trappola che costa parecchie ore: Firefox con DNS over HTTPS attivo ignora il file hosts. Mozilla lo ha marcato esplicitamente come "wontfix". Continui quindi a vedere il vecchio sito e consideri fallito il test, anche se il nuovo server risponde correttamente già da un pezzo. Nel browser di prova disattiva quindi la risoluzione DNS cifrata, oppure meglio: verifica in modo indipendente dal browser. curl può forzare la risoluzione a ogni singola chiamata, senza alcun file hosts:
curl -sI --resolve example.com:443:203.0.113.10 https://example.com/
Che cosa dovresti controllare in questo stato: home page, almeno due sottopagine, un articolo con immagini, il login su /wp-login.php, il backend, un modulo di contatto e, in caso di negozio, un articolo di prova fino alla cassa. Le immagini meritano un'attenzione particolare, perché gli upload mancanti nel backend non saltano all'occhio.
Che cosa il test con il file hosts esplicitamente non copre: tutto ciò che accede al server dall'esterno. I webhook dei fornitori di pagamento, i cron job esterni, i crawler dei motori di ricerca e l'invio delle tue email finiscono ancora sulla vecchia destinazione. Va bene così, devi soltanto saperlo.
Lo switch: sincronizzazione differenziale e DNS
Tra la prima copia e il cambio si sono aggiunti commenti, ordini o articoli. L'ordine per uno switch pulito:
- Attiva la modalità manutenzione sul vecchio server oppure ferma almeno per un attimo ordini e commenti.
- Sincronizza di nuovo i file, questa volta bastano pochi secondi.
- Esporta di nuovo il database e importalo sul nuovo server.
- Svuota le cache:
wp cache flushewp rewrite flush --hard. - Verifica ancora una volta con curl contro il nuovo IP.
- Cambia i record A e AAAA.
Non dimenticare il record AAAA. Se resta sul vecchio indirizzo IPv6, tutti i visitatori con IPv6 continuano a finire sul vecchio server, mentre i visitatori IPv4 vedono il nuovo sito. Questo genera esattamente il tipo di quadro d'errore in cui due persone sedute una accanto all'altra vedono siti diversi.
Lascia acceso il vecchio server per almeno una settimana, senza modifiche. È la tua via di ritorno e il tuo archivio.
Come capire che è andata davvero a buon fine
Non "il sito si carica", ma prove misurabili:
dig +short A example.com
dig +short AAAA example.com
curl -sI https://example.com/ | head -n 12
Negli header non deve comparire nessun 301 verso un vecchio dominio o verso http://. Poi chiedi al database quale indirizzo WordPress stesso ritiene corretto:
wp option get siteurl
wp option get home
Un test affidabile sui resti sfuggiti è una ricerca a vuoto. Se segnala zero sostituzioni, il vecchio indirizzo non è davvero più registrato da nessuna parte:
wp search-replace 'vecchio-dominio.it' 'vecchio-dominio.it' --all-tables --dry-run --report-changed-only
Inoltre controlla il certificato, compresi periodo di validità e nomi coperti:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates -subject
E infine il log degli errori del web server, mentre navighi il sito per cinque minuti. Se lì non compare nulla di nuovo, è fatta. Se invece qualcosa si inceppa, Risolvere 502 Bad Gateway è il punto di partenza più frequente, di solito perché il socket PHP-FPM nel blocco nginx porta ancora il percorso della vecchia versione di PHP.
Se qualcosa va storto: la via di ritorno
La via di ritorno consiste in un'unica operazione, ed è per questo che il passo 1 era così importante: rimetti i record A e AAAA sul vecchio IP. Con un TTL di 300 secondi la maggior parte dei visitatori torna sul vecchio server entro cinque minuti, e quel server ha continuato a girare senza modifiche.
C'è esattamente un caso in cui la cosa non è pulita: quando sul nuovo server sono già nati dati che sul vecchio non esistono. Nuovi ordini, commenti, registrazioni. Dopo un rollback questi dati esistono soltanto nel nuovo sistema. Per questo si decide entro i primi minuti oppure non si decide affatto. Ed è per questo che serve la modalità manutenzione durante lo switch: tiene stretta la finestra temporale in cui i due insiemi di dati possono divergere.
Se comunque devi tornare indietro più tardi, esporta dal nuovo server soltanto le tabelle interessate e caricale su quello vecchio, invece di rispedire indietro l'intero database. Un rollback puro da un vecchio backup completo cancella tutto quello che è successo nel frattempo.
I messaggi di errore, parola per parola
Error establishing a database connection
Le credenziali oppure DB_HOST nel file wp-config.php non corrispondono al nuovo server. Spesso lì c'è ancora l'IP di un server di database esterno al posto di localhost.
ERROR 1273 (HY000): Unknown collation: 'utf8mb4_0900_ai_ci'
Un dump di MySQL 8 viene importato in MariaDB. Imposta la collation nel dump su utf8mb4_unicode_ci con sed.
Error: This does not seem to be a WordPress installation.
WP-CLI è stato avviato nella directory sbagliata oppure i file non si trovano dove pensi. Lavora con --path=/var/www/html.
ERR_TOO_MANY_REDIRECTS
Quasi sempre una contraddizione tra siteurl nel database, una costante nel file wp-config.php e il redirect nel web server. Riguarda inoltre le installazioni dietro un proxy che non percepiscono lo stato HTTPS e quindi rimbalzano all'infinito da http a https e ritorno.
La home page si carica, tutte le sottopagine restituiscono 404
Il classico nel passaggio da Apache a nginx. Il file .htaccess con le regole dei permalink viene ignorato da nginx, lì serve try_files $uri $uri/ /index.php?$args; nel blocco location.
Il file caricato eccede la direttiva upload_max_filesize in php.ini.
La configurazione PHP non è stata migrata insieme al resto. Imposta upload_max_filesize, post_max_size e memory_limit sui valori del vecchio server.
Pagina bianca, nessuna voce nel log
Di solito un drop-in che punta nel vuoto: wp-content/object-cache.php oppure advanced-cache.php. Rinominalo e ricarica.
Una migrazione che si svolge così non ha alcuna finestra senza sito web. L'unico momento percepibile è il cambio DNS, e la sua durata l'hai stabilita tu stesso due giorni prima.
Domande frequenti
Quanto tempo serve perché il cambio DNS abbia effetto ovunque?
Perché non posso sostituire il dominio nel database con un semplice comando SQL?
Devo davvero sostituire il dominio se cambia solo il server?
Come ottengo un certificato Let's Encrypt se il dominio punta ancora al vecchio server?
Ho modificato il file hosts, ma continuo a vedere il vecchio sito. Da cosa dipende?
Qual è la via di ritorno più rapida se dopo il cambio qualcosa non funziona?
2026 KernelHost GmbH. Tutti i diritti riservati. Questa guida è protetta dal diritto d'autore. La ripubblicazione su altri siti web, anche parziale o in forma modificata, non è consentita senza il nostro consenso scritto. Le citazioni con indicazione della fonte e un link sono le benvenute.

