Tutorial

How to Migrate WordPress to a VPS

By Amith Kumar24 July 202613 min read
Self-hostingWordPress
How to Migrate WordPress to a VPS
Verified 24 Jul 2026 · Ubuntu 24.04 LTS · nginx 1.24.0, MariaDB 10.11.14

What you will have

This tutorial shows you how to migrate WordPress to a VPS the safe way: the same live site you have now, running on a slice you own instead of a shared host you rent a slice of. Same posts, same pages, same uploads, same theme and plugins, same domain, now served by your own nginx and PHP-FPM over a real HTTPS certificate, with a database only your site can touch. Nothing about the site changes for a visitor. Everything about who controls it changes for you.

Here is the destination, shown first so you know what a finished move looks like. The screenshots are not mockups. They are captured from a genuine migration we ran on our lab box, moving a demo bakery site onto a fresh install at wp.boxlab.space while writing this.

# the migrated site, now answering from your slice
curl --http2 -sI https://your-domain/
The migrated site answering over HTTP/2 with 200 OK, the nginx server header and the api.w.org link header that only a live WordPress emits, then wp option get blogname reading Maple Street Bakery, proving the same content is now served from the slice at wp.boxlab.space
The migrated public site at wp.boxlab.space, Maple Street Bakery on the default Twenty Twenty-Five block theme, showing the moved posts over real HTTPS with a valid padlock, the genuine front end a visitor still sees after the move

That response is the site you already had, now coming from hardware you control: WordPress answering over HTTP/2, your security headers in place, and the api.w.org link header that only a live WordPress emits. And here is the same site's public front page and its wp-admin, moved across intact.

What this tutorial covers
  • Preparing a target slice with the LEMP stack and an empty landing spot for the incoming site.
  • Exporting the two halves of any WordPress site: the files and the database.
  • Transferring both to the slice with rsync, then importing them into place.
  • Rewriting URLs with wp search-replace when the address changes, and flushing permalinks.
  • Verifying the copy, then cutting over DNS with a final delta sync so nobody sees an outage.

A WordPress site is really two things bolted together: a folder of files and a database. Migrating it well is just moving both cleanly, pointing the new copy at itself, and switching traffic over once you have proven the copy works.

# the site's own admin, moved intact
wp option get home
The migrated wp-admin Posts list at wp.boxlab.space, logged in as the site admin, showing the four posts that came across in the migration, WordPress 7.0.2, the whole admin moved intact and served over HTTPS in the ServerCake browser chrome

Before you begin

This is a hands-on move, so it assumes you can reach both ends over SSH. None of it is heavy.

Before you start
  • An existing WordPress site to move (the source), with SSH or SFTP access to its files and a way to run mysqldump on its database. Most shared hosts and cPanel accounts give you both.
  • A slice with the LEMP stack already installed, following how to install WordPress on a VPS. You need nginx, PHP-FPM and MariaDB running, plus a database and a least-privilege user for the incoming site.
  • The domain the site uses, with access to its DNS records so you can repoint it at the slice at the end.
  • SSH access to the slice as a normal user cleared for sudo, and about an hour.

Everything below runs as a normal sudo user over SSH. The source commands run on your old host, the slice commands run on your slice, and the transfer step bridges the two.

How to migrate WordPress to a VPS

The whole move is eight small steps, and it helps to see them before you start. You will migrate WordPress to a VPS by preparing the target, copying both halves of the site, wiring them together, and only then switching traffic:

  1. Prepare the target slice: LEMP plus an empty database and web root.
  2. Export the files from the old host as one archive.
  3. Export the database with mysqldump.
  4. Transfer both to the slice with rsync.
  5. Import the files into the web root and the database into MariaDB.
  6. Point wp-config.php at the new database, then rewrite URLs if the address changes.
  7. Flush permalinks and verify the copy over HTTPS.
  8. Lower your DNS time-to-live, run a final delta sync, and repoint DNS.

The reason for that order is downtime. The old site keeps serving traffic the whole time you build the copy. You only switch DNS at the very end, after a last sync catches anything that changed while you worked.

Prepare the target slice

Your slice should already run nginx, PHP-FPM and MariaDB from the install tutorial. What the incoming site needs is a database of its own and an empty web root to land in. Create the database and a user scoped to it alone, exactly as a fresh install would, then give the incoming files somewhere to go.

# an empty landing spot for the incoming site
cd /var/www && sudo -u www-data wp core download --path=wp
sudo -u www-data wp config create --path=wp \
     --dbname=wordpress --dbuser=wpuser --dbpass='your-db-password' --dbhost=localhost
sudo chmod 640 wp/wp-config.php
Preparing the target slice: wp core download fetching WordPress 7.0.2 into the web root, wp config create writing wp-config.php pointed at the new wordpress database, and SHOW TABLES confirming the database is still empty, an empty landing spot for the incoming site

Downloading a fresh core here is deliberate. It gives you clean, current WordPress files to receive your themes, plugins and uploads on top of, so you are not carrying across any core files the old host may have modified. The database stays empty for now; the import fills it in a moment.

Checkpoint You should have a wordpress database with no tables yet, a wpuser whose reach stops at that database, and a /var/www/wp folder holding a fresh WordPress core with a wp-config.php pointed at the new database.

Export your site from the old host

On the old host, a WordPress site is a directory and a database, so you export both. The files that matter live under wp-content: your uploads, your themes and your plugins. Archive that folder into a single file you can move in one hop.

cd /path/to/your/site
tar -czf ~/wp-content.tar.gz wp-content
du -sh wp-content
Exporting the files on the old host: tar packing wp-content into a 15M archive, then du showing the uploads, themes and plugins that make up the file half of a WordPress site

Now dump the database. Read the database name, user and host straight out of the old site's wp-config.php if you are not sure of them, then run mysqldump. The --single-transaction flag takes a consistent snapshot without locking the site, so the old site keeps serving while you export.

mysqldump --single-transaction --quick \
     --default-character-set=utf8mb4 OLD_DB_NAME > ~/database.sql
Exporting the database on the old host with mysqldump using a single consistent transaction so the live site is not locked, producing a 96K database.sql whose header shows the MariaDB 10.11.14 source
Checkpoint You should now have two files in your home directory on the old host: wp-content.tar.gz with your uploads, themes and plugins, and database.sql with every post, page, comment and setting.

Transfer the files and database to the slice

With both halves exported, move them to the slice. rsync over SSH is the right tool: it copies only what it needs, verifies as it goes, and can resume if the link drops. Run this from the old host, pointing at your slice.

rsync -avz ~/wp-content.tar.gz ~/database.sql \
     deploy@your-slice:/home/deploy/
Transferring both halves of the site to the slice with rsync over SSH: the wp-content archive and the database dump sent in one incremental pass, with the sent and total byte counts reported

For the file tree specifically, you can also rsync the unpacked wp-content directory straight across rather than moving the archive, which is handy for a large uploads folder because a re-run only sends what changed. Either way, both halves of the site are now sitting on the slice, ready to import.

Import WordPress on the slice

On the slice, unpack the files into the web root you prepared, laying your uploads, themes and plugins over the fresh core. Then hand everything to the web server user so PHP-FPM can read and write it.

# unpack the files into the web root
cd /var/www/wp
sudo tar -xzf /home/deploy/wp-content.tar.gz -C /var/www/wp
sudo chown -R www-data:www-data /var/www/wp
Importing the files on the slice: tar unpacking wp-content into /var/www/wp over the fresh WordPress core, then chown handing every file to the www-data web server user

Now import the database. Load the dump into the empty wordpress database you created, then confirm wp-config.php points at that database and the table prefix in the file matches the one in the dump.

sudo mysql wordpress < /home/deploy/database.sql
sudo -u www-data wp config get DB_NAME --path=/var/www/wp
sudo -u www-data wp db check --path=/var/www/wp
Importing the database on the slice: loading database.sql into the empty wordpress database, then wp config get and wp db check confirming wp-config.php reads DB_NAME wordpress and the imported tables check out cleanly

A Success: Database checked back from wp-cli means the tables imported cleanly and the config can reach them. The site's content is now on the slice, though it still thinks it lives at its old address, which the next step fixes.

One detail catches people out here: the table prefix. Most sites use the default wp_, but some hosts set a random prefix like wp_x9f2_ for a little extra obscurity, and the value in your new wp-config.php must match the prefix in the dump exactly. If wp-cli reports no tables after a clean import, open the dump and check the CREATE TABLE lines, then set $table_prefix in wp-config.php to the same value. Getting this one line right is the difference between a working import and a blank white screen.

Checkpoint Your slice's database now holds all the site's tables, and wp-config.php points at it with the right name, user and table prefix. If wp db check cannot connect, re-check the four DB_ lines against the user you created.

Fix the URLs and go live

If your domain stays the same, which is the common case, you can skip straight to permalinks: the site already knows its own address. If the address changes at all, and it does whenever you preview on the slice's own hostname first or consolidate onto a new domain, the old URL is baked into the database in dozens of places.

Rewrite it in one pass with wp search-replace, which understands WordPress serialized data and will not corrupt it the way a blind find-and-replace would. Run it dry first to see the count, then for real.

sudo -u www-data wp search-replace 'https://old-domain' 'https://your-domain' \
     --all-tables --dry-run --path=/var/www/wp
sudo -u www-data wp search-replace 'https://old-domain' 'https://your-domain' \
     --all-tables --report-changed-only --path=/var/www/wp
Rewriting the site URL safely with wp search-replace: a dry run reporting 13 replacements to be made across all tables, then the live run making 13 replacements across wp_options, wp_posts and wp_users without corrupting any serialized data

With the URLs correct, flush the permalink rules so pretty links resolve against the new nginx config, and clear any cached values.

sudo -u www-data wp cache flush --path=/var/www/wp
sudo -u www-data wp rewrite flush --hard --path=/var/www/wp
Going live: wp cache flush and wp rewrite flush rebuilding the permalink rules, then curl showing plain HTTP redirected 301 to HTTPS and the migrated home page answering 200 over HTTP/2 on the slice

Verify and cut over with no downtime

Before you touch DNS, prove the copy works. On the slice, list the posts to confirm the content came across, and check that siteurl and home now read your target address. Then request the front page and a single post over HTTPS to confirm nginx serves them and permalinks resolve.

sudo -u www-data wp post list --path=/var/www/wp \
     --post_type=post,page --fields=ID,post_title,post_status
curl -sI https://your-domain/your-first-post/ | head -1
Verifying the copy on the slice: wp post list showing the migrated bakery posts and pages, siteurl and home now reading https://wp.boxlab.space, and a single post answering 200 at its pretty permalink over HTTPS
A single migrated post, We now open at seven on weekdays, rendered at its pretty permalink on wp.boxlab.space over real HTTPS, proving the URL rewrite and permalink flush worked and the content resolves at its own address

Two HTTP 200 responses, one for the home page and one for a real post at its pretty permalink, mean the migrated site is fully working on the slice. This is the moment to cut over, and the trick to doing it without an outage is preparation.

A day ahead, lower your domain's DNS time-to-live to five minutes in your DNS panel so the switch propagates fast. When you are ready, run one last delta sync to catch anything that changed on the old site while you worked, confirm the slice still answers, then repoint the domain's A record at your slice.

# the final delta sync before you flip DNS
rsync -a /path/to/old/wp-content/ deploy@your-slice:/var/www/wp/wp-content/
curl -sI https://your-domain/ | head -1
Cutting over with no downtime: a final rsync delta transferring zero files because the two copies already match, then curl confirming the slice still answers 200 over HTTP/2, the point at which it is safe to repoint DNS

A delta sync that transfers nothing means the two copies already match, so you can flip DNS knowing the slice is current. Point the A record at your slice's public IP and, if you want to watch it move, run dig +short your-domain until it returns the new address.

Because the old host keeps serving until DNS moves, and the low TTL means the change takes minutes not hours, visitors never hit a closed door. Traffic drains from the old host to the slice as caches expire, and the site you just built takes over.

Do not skip this Lower the DNS TTL and run the final delta sync before you repoint DNS, not after. Skipping the last sync loses any comment, order or post made on the old site during the migration window, and a high TTL can strand visitors on the old host for hours after you have moved on.

After the move

The site is live on your slice, so give it the habits that keep it healthy. Take a first backup now, both the files and a fresh database dump, and store it off the slice, because a backup on the same machine is not a backup. Then turn on automatic updates so the site stays patched without you watching it.

# your first backup on the new slice
sudo tar -czf ~/wp-backup.tar.gz -C /var/www/wp wp-content
sudo -u www-data wp db export ~/wordpress.sql --path=/var/www/wp
sudo -u www-data wp plugin auto-updates enable --all --path=/var/www/wp
sudo -u www-data wp theme auto-updates enable --all --path=/var/www/wp
After the move: a first off-slice backup with tar plus wp db export, then wp enabling automatic updates for all plugins and themes so the migrated site stays patched on its own

Leave the old host running for a week or two as a safety net, then decommission it once you are sure the slice is serving all your traffic and your backups are landing. Cancel the old plan only after you have a working backup off the slice and have watched the new site handle real visitors. That is a complete move: the same site, now on infrastructure you own, with a clean database, a real certificate and a backup in hand.

Frequently asked questions

Will migrating WordPress to a VPS cause downtime? It does not have to. The method in this tutorial builds the whole copy on the slice while the old host keeps serving traffic, so the only switch is a DNS change at the very end. Lower your DNS time-to-live to five minutes a day before, run a final delta sync just before you flip the record, and visitors move from the old host to the slice over a few minutes as caches expire. Done in that order, nobody sees an outage.
Do I need a migration plugin to move WordPress to a VPS? No. A plugin can be convenient on a host where you have no shell, but on a VPS you have SSH, and the manual path in this tutorial is more reliable for a large site. Plugins pack everything into one archive that can time out or run short of memory on a big uploads folder. Copying the files with rsync and the database with mysqldump has no size ceiling, resumes if a transfer drops, and shows you exactly what moved.
Why use wp search-replace instead of a SQL find and replace? Because WordPress stores some settings as serialized PHP, where each string is prefixed with its own length. A plain SQL REPLACE changes the text but not the recorded length, which breaks the serialized value and can blank out widgets, theme options or plugin settings. wp search-replace understands the format, rewrites the string and fixes the length in the same pass, so it is safe across every table. Always run it with --dry-run first to see the count.
What should I do with the old host after the migration? Keep it running, untouched, for a week or two as a fallback. If anything looks wrong on the slice, you can repoint DNS back in minutes while you investigate. During that window, confirm the slice handles real traffic, checkout and forms work, and your off-slice backups are landing on schedule. Only then cancel the old plan. Decommissioning too early removes your safety net before you have proven the new setup under load.

Running your site on your own slice means you own the whole stack: the files, the database, the certificate and the bill, with no per-site markup and no black box. When you are ready to make the move, a ServerCake slice gives you a clean Ubuntu 24.04 box in India, billed in rupees with GST, ready for exactly the steps above.


Move WordPress to your own slice

Migrate WordPress to a slice

Move your site off shared hosting for good. Spin up a ServerCake slice, a clean Ubuntu 24.04 box in India billed in rupees with GST, and migrate WordPress onto infrastructure you own, with a clean database and real HTTPS.

Get early access

This guide is provided for educational purposes and offered as is, without warranty of any kind. The commands change the configuration of a server you control, and some can lock you out if run out of order. Test on a non-production machine, keep a second session open, and take a snapshot or backup before you begin. You are responsible for changes made to your own systems; ServerCake accepts no liability for any loss, downtime, lockout, or damage arising from following this guide or running the accompanying script.

Spotted something out of date or have a question?

Built by the ServerCake team

Cloud that speaks India.

ServerCake is India's KYC-verified cloud: VMs and managed databases priced in ₹, billed with GST, on India-resident infrastructure. Reserve your spot for early access.

Reserve your spot