Shared hosting is convenient until you need more resources, a newer PHP version or control over caching and security. Moving WordPress to a VPS is a manual but predictable process: copy the files and database, recreate the stack, test the copy privately, then point DNS at the new server. In this tutorial you will migrate a WordPress site from shared hosting to an Ubuntu 24.04 VPS running Nginx, PHP 8.3-FPM and MariaDB, with minimal downtime and HTTPS from Let's Encrypt.

Prerequisites

To follow this tutorial, you will need:

  • A VPS running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has sudo privileges. 1 GB of RAM is enough for a small site; 2 GB is more comfortable.
  • Access to your shared hosting account: SSH if your host offers it, or otherwise the control panel (cPanel, Plesk or similar) and SFTP.
  • Access to your domain's DNS settings.
  • The domain name of the site (your_domain in this guide) and the VPS public IP address (your_server_ip).

The migration keeps the same domain. If you are also changing domains, you will need an extra search-and-replace step, covered at the end of Step 6.

Step 1 - Lowering the DNS TTL

The TTL (time to live) of a DNS record controls how long resolvers cache it. If your A record has a TTL of several hours, some visitors will keep reaching the old host long after you switch. At least 24 hours before the migration, lower the TTL of the A records for your_domain and www.your_domain to 300 seconds at your DNS provider.

Check the current TTL (the second column):

dig your_domain A +noall +answer
your_domain.		300	IN	A	203.0.113.10

Step 2 - Preparing the VPS

Install Nginx, MariaDB, PHP 8.3-FPM and the PHP extensions WordPress uses. Ubuntu 24.04 ships PHP 8.3, so no extra repository is needed:

sudo apt update
sudo apt install nginx mariadb-server php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd php8.3-intl php8.3-mbstring php8.3-xml php8.3-zip unzip

Open HTTP and HTTPS in the firewall, keeping SSH allowed:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Create an empty database and a user for WordPress. Use your own strong password in place of your_strong_password:

sudo mariadb
CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wordpress'@'localhost' IDENTIFIED BY 'your_strong_password';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Install WP-CLI, which you will use to import the database and check the site:

curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --info

Create the site directory:

sudo mkdir -p /var/www/your_domain

Step 3 - Backing up the site on shared hosting

From this point on, avoid changes on the old site (new posts, orders, comments) until the migration is done, or they will be lost. For a shop, put the site in maintenance mode first.

With SSH access

Most hosts that offer SSH also have WP-CLI or mysqldump. Log in and export the database from the WordPress directory, usually public_html:

ssh your_hosting_user@your_old_host
cd ~/public_html
wp db export ~/wordpress.sql

If wp is not available, read the database name, user and password from wp-config.php and use mysqldump:

grep -E "DB_(NAME|USER|HOST)" wp-config.php
mysqldump -u your_db_user -p --single-transaction --default-character-set=utf8mb4 your_db_name > ~/wordpress.sql

Log out of the shared host when the export is done.

Without SSH access

In the control panel, open phpMyAdmin, select the WordPress database, choose Export, keep the Quick method and SQL format, and download the file as wordpress.sql. Then download the whole WordPress directory with an SFTP client such as FileZilla, or compress it in the panel's File Manager and download the archive. Upload both to the VPS with scp:

scp wordpress.sql wordpress-files.zip your_user@your_server_ip:~

Then skip to the database import in Step 4, and unzip the archive into /var/www/your_domain.

Step 4 - Copying files and database to the VPS

If the old host has SSH, pull everything directly from the VPS with rsync, which shows progress and can resume if the connection drops. Run this on the VPS:

sudo rsync -avz --progress your_hosting_user@your_old_host:public_html/ /var/www/your_domain/
sudo rsync -avz your_hosting_user@your_old_host:wordpress.sql ~/wordpress.sql

The trailing slash on public_html/ copies its contents rather than the directory itself. If your host uses a non-standard SSH port, add -e "ssh -p 2222" with the right port.

Check that the core WordPress files arrived:

ls -d /var/www/your_domain/{wp-admin,wp-content,wp-includes,wp-config.php}
/var/www/your_domain/wp-admin
/var/www/your_domain/wp-config.php
/var/www/your_domain/wp-content
/var/www/your_domain/wp-includes

Give the files to the web server user and set standard permissions:

sudo chown -R www-data:www-data /var/www/your_domain
sudo find /var/www/your_domain -type d -exec chmod 755 {} +
sudo find /var/www/your_domain -type f -exec chmod 644 {} +

Import the database into the new MariaDB database:

sudo mariadb wordpress < ~/wordpress.sql

Confirm the tables are there. The prefix is usually wp_, but shared hosts often use a random one such as wpxy_:

sudo mariadb wordpress -e "SHOW TABLES;"

Step 5 - Updating wp-config.php

The copied wp-config.php still has the shared host's database credentials. Open it:

sudo nano /var/www/your_domain/wp-config.php

Update the database settings to match the database you created in Step 2:

define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wordpress' );
define( 'DB_PASSWORD', 'your_strong_password' );
define( 'DB_HOST', 'localhost' );

Do not change $table_prefix; it must match the table names you just imported. While you are in the file, remove lines added by the old host that do not apply to the VPS, such as host-specific caching constants or WP_CACHE settings pointing to a hosting plugin you will not use.

Protect the file, since it contains the password:

sudo chmod 640 /var/www/your_domain/wp-config.php

Check that WordPress can reach the database:

cd /var/www/your_domain
sudo -u www-data wp db check
sudo -u www-data wp option get siteurl
Success: Database checked.
https://your_domain

Step 6 - Configuring Nginx

Create a server block for the site:

sudo nano /etc/nginx/sites-available/your_domain
server {
    listen 80;
    listen [::]:80;
    server_name your_domain www.your_domain;

    root /var/www/your_domain;
    index index.php;

    client_max_body_size 64m;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }
}

The try_files line replaces the rewrite rules from the old .htaccess file, which Nginx ignores. Enable the site, remove the default one, test and reload:

sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Match PHP's upload limit to the client_max_body_size above. Open the FPM php.ini:

sudo nano /etc/php/8.3/fpm/php.ini

Set these values:

upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
sudo systemctl restart php8.3-fpm

If you are changing domains as part of the migration, replace the old URL in the database now. Run it with --dry-run first to see how many replacements it will make:

sudo -u www-data wp search-replace 'https://old_domain' 'https://your_domain' --skip-columns=guid --dry-run

Then repeat without --dry-run. WP-CLI handles serialized PHP data correctly, which a plain SQL REPLACE would corrupt.

Step 7 - Testing the site before switching DNS

Test the new server without affecting visitors by pointing only your own computer at it. Add a line to your local hosts file (/etc/hosts on Linux and macOS, C:\Windows\System32\drivers\etc\hosts on Windows):

your_server_ip  your_domain www.your_domain

If the site already uses HTTPS, your browser will show a certificate warning, because the VPS has no certificate yet. Accept it temporarily, or test from the command line:

curl -sI --resolve your_domain:80:your_server_ip http://your_domain/
HTTP/1.1 301 Moved Permanently
Server: nginx/1.24.0 (Ubuntu)
Location: https://your_domain/

A redirect to HTTPS is expected at this point. Check the site in the browser: log in to /wp-admin, open a few posts and pages, check that images load, submit a contact form, and for shops, run through the checkout. Watch for errors while you click around:

sudo tail -f /var/log/nginx/error.log

Check that plugins still work on PHP 8.3, which may be newer than the old host's version:

sudo -u www-data wp plugin list

Once everything works, remove the line from your hosts file.

Step 8 - Switching DNS and enabling HTTPS

At your DNS provider, change the A records for your_domain and www.your_domain to your_server_ip. If you use IPv6, add or update AAAA records too, or delete old AAAA records that still point to the shared host. Check the change from the VPS:

dig +short your_domain
dig +short www.your_domain
your_server_ip
your_server_ip

With the TTL at 300 seconds, most visitors reach the new server within minutes. Now that the domain points to the VPS, request a Let's Encrypt certificate. Certbot's Nginx plugin installs it and adds the HTTP to HTTPS redirect:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your_domain -d www.your_domain

Verify HTTPS and automatic renewal:

curl -sI https://your_domain/ | head -n 1
sudo certbot renew --dry-run
HTTP/2 200

If the site was served over plain HTTP before, switch its URLs to HTTPS in the database to avoid mixed content warnings:

sudo -u www-data wp search-replace 'http://your_domain' 'https://your_domain' --skip-columns=guid

Step 9 - Finishing the move

Shared hosts provide some services that a VPS does not include by default:

  • Email sending: shared hosts relay WordPress emails (password resets, order notifications) through their own mail server. On the VPS, install an SMTP plugin and send through your email provider or a transactional email service, then send a test email.
  • Scheduled tasks: WP-Cron runs on page visits, as before. For reliable schedules on a low-traffic site, disable it in wp-config.php with define( 'DISABLE_WP_CRON', true ); and run it from the system cron instead.
  • Backups: the old host's automatic backups no longer apply. Set up your own database and file backups, stored outside the VPS.

Keep the shared hosting account active for at least a week, until you are sure no traffic or email depends on it, then download a final backup and close it. Raise the DNS TTL back to its previous value, such as 3600 seconds.

Troubleshooting

  • "Error establishing a database connection": the credentials in wp-config.php do not match the database. Test them directly with mariadb -u wordpress -p wordpress.
  • Home page works but every other page returns 404: Nginx is missing the try_files line from Step 6.
  • Redirect loop after enabling HTTPS: the old host may have left a plugin or wp-config.php constant that forces HTTPS behind a proxy. Look for $_SERVER['HTTPS'] overrides in wp-config.php and remove them.
  • White screen or 500 error: a plugin is incompatible with PHP 8.3. Check /var/log/nginx/error.log for the file name, then deactivate that plugin with sudo -u www-data wp plugin deactivate plugin-name.

Conclusion

Your WordPress site now runs on its own Ubuntu 24.04 VPS with Nginx, PHP 8.3-FPM, MariaDB and a Let's Encrypt certificate, and DNS points to the new server. Next, take advantage of the extra control: add a page cache and Redis object cache, harden the site with Fail2ban and correct Nginx rules, and schedule automated off-server backups.