analyst@unixshadow:~$ wp-migrate –old-box –new-box –dns-flip # all addresses below are placeholders — use your own
Migrating WordPress to a New Server, Without the Downtime Panic
A Real Move: Old VPS to New VPS, Files, Database, DNS, and HTTPS
A WordPress migration is three moves, not one: the files, the database, and the DNS. Do them in the wrong order and the site is down while you debug. Do them in this order — build the new box, copy everything, test it before anyone can see it, then flip the DNS — and the cutover is a boring non-event, which is exactly what you want. This guide walks a real move from an old cloud VPS to a new one, with the steps that actually matter and the gotchas that actually bite. Every address, hostname, and username in this post is a placeholder. Substitute your own.
# the plan, before you touch anything — the migration is won in the planning
1. Record the old box. WordPress version, PHP version, database version, web server and its version, the list of active plugins and the theme, and the size of the database and of
2. Lower the DNS TTL. At least 24 hours before the move, set the site’s DNS TTL to 300 seconds. DNS resolvers cache the old address for the TTL’s duration, so a 24-hour TTL means a 24-hour cutover. A 300-second TTL means the world sees the new box within minutes of the flip.
3. Freeze content. Agree a window where nobody posts, comments, or edits — or accept that anything written after the database dump is lost. Comments are the usual casualty: a reader comments on the old box after you copied the database, and that comment never reaches the new one.
4. Back up the old box, twice. A database dump and a file archive, stored somewhere that is not either server. The migration’s rollback plan is “point DNS back at the old box”, and that plan only works if the old box is still running and untouched until the new one is proven.
wp-content. The new box has to match or exceed all of them — a PHP version jump during a migration is two changes at once, and two changes at once is how you end up debugging the wrong thing.2. Lower the DNS TTL. At least 24 hours before the move, set the site’s DNS TTL to 300 seconds. DNS resolvers cache the old address for the TTL’s duration, so a 24-hour TTL means a 24-hour cutover. A 300-second TTL means the world sees the new box within minutes of the flip.
3. Freeze content. Agree a window where nobody posts, comments, or edits — or accept that anything written after the database dump is lost. Comments are the usual casualty: a reader comments on the old box after you copied the database, and that comment never reaches the new one.
4. Back up the old box, twice. A database dump and a file archive, stored somewhere that is not either server. The migration’s rollback plan is “point DNS back at the old box”, and that plan only works if the old box is still running and untouched until the new one is proven.
Do not delete, terminate, or stop paying for the old server until the new one has served real traffic for at least a few days. The old box is the rollback plan.
# build the new box — the stack first, WordPress second
Install the stack, in this order: web server (nginx or Apache), PHP with the extensions WordPress needs (
Create the database and user on the new box before anything is copied:
Loopback-only database access, a dedicated app user with rights to one database only, no anonymous users, no test database — the same hardening you would apply to any fresh install.
Point the web server at an empty document root and confirm it serves a test page before WordPress arrives. If the stack does not serve a static file, it will not serve WordPress, and debugging the stack with WordPress in the way is slower than debugging it empty.
php-mysqlnd, php-gd, php-xml, php-mbstring, php-curl, php-zip, php-intl — check with wp core check-update and the Site Health screen later), the database server, and WP-CLI. Match the PHP version to the old box first; upgrade PHP after the migration, not during it.Create the database and user on the new box before anything is copied:
mysql -u root -p -e "CREATE DATABASE wordpress; CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'a-strong-password'; GRANT ALL ON wordpress.* TO 'wp_user'@'localhost';"Loopback-only database access, a dedicated app user with rights to one database only, no anonymous users, no test database — the same hardening you would apply to any fresh install.
Point the web server at an empty document root and confirm it serves a test page before WordPress arrives. If the stack does not serve a static file, it will not serve WordPress, and debugging the stack with WordPress in the way is slower than debugging it empty.
# copy the files — wp-content is the site; the rest is replaceable
Archive on the old box, copy the archive, extract on the new one. rsync over SSH is the usual tool — it resumes, it shows progress, and it preserves permissions:
Or tar it, copy the tarball, and extract it. Either way, what matters is
Permissions, the thing everyone forgets. The web server user (often
Check the size before you start.
rsync -avz --progress old-box:/var/www/ new-box:/var/www/Or tar it, copy the tarball, and extract it. Either way, what matters is
wp-content — themes, plugins, uploads — because WordPress core itself is replaceable: a fresh core download is identical to the old one, and a clean core is often the better choice, since it guarantees no modified core files survive the move. Copy wp-config.php separately and edit it on the new box, or write a fresh one with the new database credentials.Permissions, the thing everyone forgets. The web server user (often
nginx or www-data) has to read the files and write to wp-content/uploads. After the copy, set ownership to the web server user and check an upload works — a migration that serves pages but cannot upload media is a migration that is not finished.Check the size before you start.
du -sh /var/www/wp-content/uploads tells you how long the copy will take. A multi-gigabyte uploads directory over a slow link is an overnight rsync, not a five-minute job, and discovering that mid-window is how migrations run over.# copy the database — dump, transfer, import, then fix the URLs
Dump on the old box:
Transfer and import on the new box:
Then fix the URLs — if the domain is changing, or if the site was ever served from a different address. Never edit the SQL dump by hand: WordPress stores serialised data (theme options, widget settings), and a hand-edited URL that changes length breaks the serialisation and silently corrupts the option. Use WP-CLI, which handles serialised data correctly:
Run it with
Verify the import. Count the tables on both boxes, count the rows in
mysqldump -u wp_user -p wordpress > wordpress.sqlTransfer and import on the new box:
mysql -u wp_user -p wordpress < wordpress.sqlThen fix the URLs — if the domain is changing, or if the site was ever served from a different address. Never edit the SQL dump by hand: WordPress stores serialised data (theme options, widget settings), and a hand-edited URL that changes length breaks the serialisation and silently corrupts the option. Use WP-CLI, which handles serialised data correctly:
wp search-replace 'http://old.example.com' 'https://www.example.com' --all-tables --preciseRun it with
--dry-run first, read the table counts, then run it for real. If the domain is not changing, this step is still worth a dry run — it shows you every place the old address appears, including the ones you forgot about.Verify the import. Count the tables on both boxes, count the rows in
wp_posts and wp_users, and confirm the new box’s wp_options has the right siteurl and home. A database that imports with errors and a site that mostly works is worse than a database that fails loudly — the missing rows show up weeks later, in the post nobody can find.The database is the site. The files are the decoration. If the database is wrong, nothing else matters — so verify it before you touch DNS.
# test before anyone can see it — the hosts-file trick
Point your own machine at the new box, without touching DNS. Add a line to your local hosts file mapping the domain to the new server’s address, and your browser will load the new box while the rest of the world still sees the old one. Click through the site: homepage, a post, a page, the admin dashboard, a search, a media upload, a login. Check the browser console for mixed-content warnings — an
Run Site Health (Tools → Site Health in wp-admin) on the new box and read it properly. It will flag the PHP version, missing extensions, and any plugin that does not like its new home — all fixable before the cutover, all painful after it.
Then remove the hosts-file line. Forgetting it means you keep testing the new box while believing you are testing the live site, which is exactly the wrong way round.
http:// asset on an https:// page is the classic post-migration tell, and it means a URL somewhere did not get replaced.Run Site Health (Tools → Site Health in wp-admin) on the new box and read it properly. It will flag the PHP version, missing extensions, and any plugin that does not like its new home — all fixable before the cutover, all painful after it.
Then remove the hosts-file line. Forgetting it means you keep testing the new box while believing you are testing the live site, which is exactly the wrong way round.
# flip the DNS, then HTTPS — the cutover, in two steps
1. Flip the DNS. Change the A record (and AAAA, if you have one) to the new server’s address. Because the TTL was lowered a day earlier, the world moves over within minutes. Watch the new box’s access log: requests arriving means the flip is working. Keep the old box running — if anything is wrong, the rollback is one DNS change, not a rebuild.
2. HTTPS on the new box. Issue the certificate after the DNS flip, not before — the ACME challenge has to reach the new box, and before the flip it reaches the old one. With the DNS pointing at the new server,
3. Set up the recurring jobs on the new box. Cron entries do not migrate themselves: the backup script, the certificate renewal check, WP-Cron’s system cron if you run it that way, and any log rotation or monitoring. A migration that forgets cron is a site that stops backing itself up the day it moves — and nobody notices until the day it is needed.
2. HTTPS on the new box. Issue the certificate after the DNS flip, not before — the ACME challenge has to reach the new box, and before the flip it reaches the old one. With the DNS pointing at the new server,
certbot or acme.sh will issue and install the certificate, and the web server should redirect HTTP to HTTPS. Then check the site over HTTPS properly: homepage, a post, wp-admin, and the browser padlock with no mixed-content warnings.3. Set up the recurring jobs on the new box. Cron entries do not migrate themselves: the backup script, the certificate renewal check, WP-Cron’s system cron if you run it that way, and any log rotation or monitoring. A migration that forgets cron is a site that stops backing itself up the day it moves — and nobody notices until the day it is needed.
# the gotchas — the ways this goes wrong
wp search-replace, or theme options and widgets corrupt silentlyhttp:// asset on an https:// page means a URL did not get replaced; check the browser console before the flipwp-content/uploads — test an upload on the new box, don’t assume# quick reference — the whole post in six lines
Plan: record versions → lower DNS TTL to 300s → freeze content → back up twice.
Build: stack, database + user, empty document root serving a test page.
Copy: rsync
Test: hosts file → click through everything → Site Health → remove the hosts line.
Cut over: flip DNS → issue the HTTPS certificate → recreate cron, backups, and mail.
Rollback: point DNS back at the old box — which is why it stays running for days.
Build: stack, database + user, empty document root serving a test page.
Copy: rsync
wp-content → dump + import the database → wp search-replace with --dry-run first.Test: hosts file → click through everything → Site Health → remove the hosts line.
Cut over: flip DNS → issue the HTTPS certificate → recreate cron, backups, and mail.
Rollback: point DNS back at the old box — which is why it stays running for days.
Series: GuidesStatus: Published 8 Nov 2026
