In the previous chapter we enhanced security and performance with tweaks to the Nginx configuration. In this article, I’m going to walk you through this step-by-step guide to migrating an existing WordPress website to a new server.

There can be lots of reasons to migrate a site. Perhaps you’re moving to a new hosting provider from an old host. If you’re moving a site to a server you’ve set up with SpinupWP, the following guide will work but I recommend using our documentation on migrating a site to a SpinupWP server for more specific instructions. I promise it will save you time and headaches. 🙂

Another good reason to migrate a site is to retire a server. We don’t recommend upgrading a server’s operating system (OS). That is, we don’t recommend upgrading Ubuntu even though Ubuntu might encourage it. The truth is a lot can go wrong upgrading the OS of a live server and it’s just not worth the trouble.

A much safer approach is to spin up a fresh server, migrate existing sites, and shut down the old server. This approach allows you to test that everything is working on the new server before switching the DNS and directing traffic to it.

If you haven’t already completed the previous chapters to fire up a fresh new server, you should start at the beginning. (Interested in a super quick and easy way to provision new servers for hosting WordPress lightning fast? Check out SpinupWP.) Let’s get started!

Securely Copying Files

Before we begin migrating files, we need to figure out the best way to copy them to the new server. You could use free file manager software like FileZilla to copy the files to your computer and then on to the next server over SFTP, but it’s quite a bit slower having to download then upload. Here we’ll use SCP.

SCP will allow us to copy the files server-to-server, without first downloading them to our local machine. Under the hood, SCP uses SSH; therefore step 1 is to generate a new SSH key so that we can connect to our old server from the new server. On the newly provisioned server, create a new SSH key using the following command:

ssh-keygen -t ed25519 -C "your_server_ip_or_hostname"

Then step 2 is to copy the public key to your clipboard. You can view the public key, like so:

cat ~/.ssh/id_ed25519.pub

For step 3, on the old server add the public key to your authorized_keys file:

sudo echo "public_key" >> ~/.ssh/authorized_keys

Then for step 4, verify that you’re able to connect to the old server from the new server using SSH.

ssh abe@pluto.turnipjuice.media

If you’re unable to connect, go back and verify the previous steps before continuing.

File Migration

We’ll start by migrating the site’s files, which includes WordPress and any other files in the web root. Issue the following command from the new server. Remember to substitute your old server’s IP address and the path to the site’s web root.

scp -r abe@pluto.turnipjuice.media:~/globex.turnipjuice.media ~/globex.turnipjuice.media

With the site’s files taken care of, it’s time to add the site to Nginx.

Nginx Configuration

There are a couple of ways you can add the site to Nginx:

  1. Create a fresh config based on chapter 3
  2. Copy the config from the old server

I recommend copying the existing configuration, as you know it works. However, starting afresh can be useful, especially if your virtual host file contains a lot of redundant directives. You can download a zip file of complete Nginx configs as a fresh starting point.

In this example I’m going to copy the existing configuration. As we did with the site data, copy the file using SCP:

scp -r abe@pluto.turnipjuice.media:/etc/nginx/sites-available/globex.turnipjuice.media ~/globex.turnipjuice.media

Next, move the file into place and ensure the root user owns it:

sudo mv globex.turnipjuice.media /etc/nginx/sites-available
sudo chown root:root /etc/nginx/sites-available/globex.turnipjuice.media

The last step is to enable the site in Nginx by symlinking the virtual host into the enabled-sites directory:

sudo ln -s /etc/nginx/sites-available/globex.turnipjuice.media /etc/nginx/sites-enabled/globex.turnipjuice.media

Before testing if our configuration is good, we should copy over our SSL certificates.

SSL Certificates

Certificate file permissions are more locked down, so you will need to SSH to the old server and copy them to your home directory first.

sudo cp /etc/letsencrypt/live/globex.turnipjuice.media/fullchain.pem ~/
sudo cp /etc/letsencrypt/live/globex.turnipjuice.media/privkey.pem ~/

Then, ensure our SSH user has read/write access:

sudo chown abe *.pem

Back on the new server, copy the certificates.

scp -r abe@pluto.turnipjuice.media:~/*.pem ~/

We’re going to generate fresh certificates using Let’s Encrypt once the DNS has switched over (see Finishing Up), so we’ll leave the certificate files in our home directory for the time being and update the Nginx configuration to reflect the new paths.

sudo nano /etc/nginx/sites-available/globex.turnipjuice.media

You’ll need to update the ssl_certificate and ssl_certificate_key directives.

ssl_certificate /home/abe/fullchain.pem;
ssl_certificate_key /home/abe/privkey.pem;

To confirm the directives are correct, once again test the Nginx config:

sudo nginx -t

If everything looks good, reload Nginx:

sudo service nginx reload

Spoof DNS

It’s a good idea to test the new server as we go. We can do this by spoofing our local DNS, which will ensure the old server remains active for your visitors but allow you to test the new server. On your local machine add an entry to your /etc/hosts file, which points the new server’s IP address to the site’s domain name:

46.101.3.65    globex.turnipjuice.media

Once updated, if you refresh the site you should see “Error establishing a database connection” because we haven’t imported the database yet. Let’s handle that next.

Before continuing, remember that the domain now points to the new server’s IP address. If you usually SSH to the server using the hostname, this will no longer work. Instead, you should SSH to each server using their IP addresses until the migration process is complete.

Database Import

Before we can perform the import, we need to create the MySQL database and its database user. On the new server, log in to MySQL using the root user:

mysql -u root -p

Create the database:

CREATE DATABASE globex CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;

Then, create the database user with privileges for the new database:

CREATE USER 'globex'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON globex.* TO 'globex'@'localhost';
FLUSH PRIVILEGES;
EXIT;

With that taken care of, it’s time to export the data. We’re going to use mysqldump to perform the database export. If you need to do anything more complex, like exclude post types or perform a find and replace on the data, I would recommend using WP Migrate.

To export the database, issue the following command from the old server, replacing the database credentials with those found in your wp-config.php file:

mysqldump --no-tablespaces -u DB_USER -p DB_NAME > ~/export.sql

Switch back to your terminal of the new server and transfer the database export file:

scp abe@pluto.turnipjuice.media:~/export.sql ~/

Finally, import the database:

mysql -u DB_USER -p DB_NAME < export.sql

If any of the database connection information is different from that of the old server you will need to update your wp-config.php file to reflect those changes. Refresh the site to confirm that the database credentials are correct. If everything is working, you should now see the site.

It’s Time to Test

You now have an exact clone of the live site running on the new server. It’s time to test that everything is working as expected.

For ecommerce sites, you should confirm that the checkout process is working and any other critical paths. Remember, this is only a clone of the live site, so anything saved to the database won’t persist, as we’ll be re-importing the data shortly.

Once you’re happy that everything is working as expected, it’s time to perform the migration.

Migrating with Minimum Downtime

On busy sites, it’s likely that the database will have changed since performing the previous export. To ensure data integrity, we need to prevent the live site from modifying the database while we carry out the migration. To do that we’ll perform the following actions:

  1. Update the live site to show a ‘Back Soon’ message
  2. Export the live database from the old server
  3. Import the live database to the new server
  4. Switch the DNS to point to the new server

To stop the live site from modifying the database we’re going to show the following ‘Back Soon’ page:

<!doctype html>
<html>
    <head>
        <title>Back Soon</title>
        <style>
          body { text-align: center; padding: 150px; }
          h1 { font-size: 50px; }
          body { background-color: #e13067; font: 20px Helvetica, sans-serif; color: #fff; line-height: 1.5 }
          article { display: block; width: 650px; margin: 0 auto; }
        </style>
    </head>

    <body>
        <article>
            <h1>Back Soon!</h1>
            <p>
                We're currently performing server maintenance.<br>
                We'll be back soon!
            </p>
        </article>
    </body>
</html>

We’ll save this as an index.html page, upload it to the web root and update Nginx to serve this file, instead of index.php.

On the old server, modify your site’s virtual host file:

sudo nano /etc/nginx/sites-available/globex.turnipjuice.media

Ensure that the index directive looks like below, which will ensure that our ‘Back Soon’ page is loaded for all requests instead of WordPress:

index index.html index.php;

Once done, reload Nginx. Your live site will now be down. If you’re using Nginx FastCGI caching, any cached pages will continue to be served from the cache. However, requests to admin-ajax.php and the WordPress REST API will fail. Therefore, you will not be able to use WordPress migration plugins such as WP Migrate to perform the migration.

Before continuing, you should confirm that your live site is indeed showing the ‘Back Soon’ page by checking it from another device or removing the entry from your /etc/hosts file, which we added earlier.

Flipping the Switch

Now that the live site is down it’s time to export and import the database once more (as we did above) so that any changes that occurred to the database while we were testing are migrated. However, this time you won’t need to create a database or database user.

Once the export/import is complete you may want to add the entry back into your /etc/hosts file (if you removed it) so that you can quickly check that the database migration was successful. Once you’re confident that everything is working as expected, log into your DNS control panel and update your A records to point to the new server. Modifying your DNS records will start routing traffic to your new server. However, keep in mind that DNS queries are cached, so anyone who has visited your site recently will likely still be routed to the old server and see the ‘Back Soon’ page. Once the user’s machine re-queries for the domain’s DNS entries they should be forwarded to the new server.

We use Cloudflare as our DNS provider, with a TTL of 300 seconds. This means that most users are routed to the new server quickly when we make a DNS change. However, if your DNS TTL is higher, I would recommend lowering it a few days prior to performing the migration. This will ensure DNS changes propagate more quickly to your users.

Finishing Up

Now that the new server is live, there are a few loose ends we need to take care of, but fortunately we’ve already covered them in previous chapters:

  1. Add a Unix cron
  2. Ensure automatic backups are running
  3. Generate a new SSL certificate using Let’s Encrypt

That’s everything there is to know about migrating a WordPress site to a new server. If you follow the steps outlined here, you should have a smooth WordPress migration with little downtime. In the final chapter of our Install WordPress on Ubuntu 24.04 tutorial, we’ll cover how to keep your server and sites operational with ongoing maintenance and monitoring.