Moving from shared hosting to a VPS gives you dedicated resources, root access and control over the software versions your site runs on. The migration itself is mostly copying files and a database, but the order matters if you want to avoid downtime and lost data. In this tutorial you will move a PHP and MySQL website (the examples use WordPress, but the steps apply to most PHP applications) from a cPanel shared host to an Ubuntu 24.04 VPS running Nginx, PHP-FPM and MySQL, test it privately, then switch DNS and enable HTTPS.

Prerequisites

To follow this guide you need:

  • A VPS running Ubuntu 24.04 LTS, such as a CubePath VPS, with a non-root user with sudo privileges and UFW enabled with OpenSSH allowed.
  • Access to your shared hosting account: the control panel, and ideally SSH access (most cPanel hosts offer it, sometimes on a non-standard port).
  • The database name, user and password your site uses. For WordPress they are in wp-config.php.
  • Access to the DNS zone of your domain, wherever it is hosted.

The guide uses these placeholders: your_domain for the domain, your_server_ip for the VPS address, old_host for the shared hosting server and cpanel_user for your hosting account user.

Step 1 - Lowering the DNS TTL

DNS resolvers cache your records for as long as the TTL says. If the A record currently has a TTL of 14400 seconds (4 hours), some visitors will keep reaching the old host for up to 4 hours after you change it. Lower it at least one full old-TTL period before the migration.

In your DNS provider, set the TTL of the A records for your_domain and www to 300 (5 minutes). Then confirm what resolvers see:

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

The second column is the TTL. Once it shows 300 or less everywhere, you can move on. Note the current IP (203.0.113.10 here): it is your rollback target.

Step 2 - Taking inventory of the shared hosting account

Write down everything the site depends on before you copy anything, because shared hosts hide a lot of configuration:

  • PHP version and extensions: in cPanel, open Select PHP Version or MultiPHP Manager.
  • Databases: every database listed under MySQL Databases.
  • Cron jobs: listed under Cron Jobs. You will recreate them on the VPS.
  • Email: whether mailboxes for your_domain are hosted on the same account (check the MX record with dig +short your_domain MX).
  • Rewrite rules: any custom .htaccess rules. Nginx does not read .htaccess, so they must be translated.
  • Subdomains and addon domains, each of which needs its own server block on the VPS.

Step 3 - Installing Nginx, PHP-FPM and MySQL on the VPS

On the VPS, install the web stack. Ubuntu 24.04 ships PHP 8.3 and MySQL 8.0. The PHP extensions below cover WordPress and most common PHP applications:

sudo apt update
sudo apt install nginx mysql-server php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip php-intl php-imagick

Allow HTTP and HTTPS through the firewall:

sudo ufw allow 'Nginx Full'

Check that both services are running:

systemctl is-active nginx php8.3-fpm mysql
active
active
active

If your site requires an older PHP version than 8.3, test it on 8.3 first. Most maintained applications support it, and staying on the distribution version means security updates arrive through apt.

Step 4 - Copying the website files

Create the document root on the VPS and give your user ownership for the copy:

sudo mkdir -p /var/www/your_domain
sudo chown "$USER":"$USER" /var/www/your_domain

If your host allows SSH, pull the files directly with rsync, which preserves timestamps and can be rerun later to copy only changes. Add -e "ssh -p 2222" if the host uses a different SSH port:

rsync -az --info=progress2 cpanel_user@old_host:public_html/ /var/www/your_domain/

If SSH is not available, download a Home Directory backup from cPanel's Backup page and upload it to the VPS with scp. Extract it in a temporary directory and locate public_html inside it (its depth depends on the backup type):

mkdir ~/cpanel-backup
tar -xzf backup-your_domain.tar.gz -C ~/cpanel-backup
find ~/cpanel-backup -maxdepth 3 -type d -name public_html

Copy that directory's contents to the document root, adjusting the path to what find printed:

rsync -a ~/cpanel-backup/public_html/ /var/www/your_domain/

If you copied with rsync over SSH, compare the number of files on both sides to make sure the copy is complete:

find /var/www/your_domain -type f | wc -l
ssh cpanel_user@old_host 'find public_html -type f | wc -l'

Both numbers should match. When the copy is done, hand the files to the user PHP-FPM runs as:

sudo chown -R www-data:www-data /var/www/your_domain

Step 5 - Migrating the database

Export the database on the shared host. Log in to it over SSH:

ssh cpanel_user@old_host

Run mysqldump there and enter the database password when prompted. --single-transaction takes a consistent snapshot of InnoDB tables without locking the site, and --no-tablespaces avoids the PROCESS privilege error that shared hosting users get on MySQL 8:

mysqldump --single-transaction --no-tablespaces -u cpanel_user_wpuser -p cpanel_user_wpdb > ~/site.sql
exit

Back on the VPS, download the dump:

scp cpanel_user@old_host:site.sql ~/site.sql

If you have no SSH access, use phpMyAdmin > Export with the SQL format and upload the file to the VPS.

Check that the dump is complete. A successful mysqldump always ends with a Dump completed line:

tail -n 1 ~/site.sql
-- Dump completed on 2026-09-25 10:41:12

On the VPS, create the database and a dedicated user. Use a new strong password (replace your_strong_password):

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

Import the dump:

mysql -u wordpress -p wordpress < ~/site.sql

Verify that the tables are there:

mysql -u wordpress -p -e "SHOW TABLES;" wordpress
+-----------------------+
| Tables_in_wordpress   |
+-----------------------+
| wp_commentmeta        |
| wp_comments           |
| wp_options            |
| wp_posts              |
...

Step 6 - Updating the application configuration

The site still points at the old database credentials. For WordPress, edit wp-config.php:

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

Update these four values to match what you created in the previous step:

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

Because the domain does not change, you do not need to search and replace URLs in the database. Other applications keep these settings in files such as .env or config.php; update them the same way.

Step 7 - Configuring the Nginx server block

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 index.html;

    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 standard WordPress .htaccess rewrite rules. If Step 2 turned up other custom rules, translate them into location blocks here.

Enable the site, disable the default one, test the syntax 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
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Step 8 - Testing the site before switching DNS

You can browse the new server under the real domain without touching DNS by overriding name resolution on your own computer only. Edit the hosts file on your workstation (/etc/hosts on Linux and macOS, C:\Windows\System32\drivers\etc\hosts on Windows) and add:

your_server_ip your_domain www.your_domain

From the command line you can do the same test without editing any file:

curl -I --resolve your_domain:80:your_server_ip http://your_domain/
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Content-Type: text/html; charset=UTF-8

In the browser, log in to the admin area, open several pages, submit a form and upload an image. Watch the PHP and Nginx logs for errors while you do it:

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

Remove the hosts file entry when you finish testing.

Step 9 - Recreating cron jobs

Recreate the cron jobs from your inventory under the www-data user so they run with the same permissions as the site:

sudo crontab -u www-data -e

For example, WordPress sites that disable the built-in pseudo-cron usually need:

*/5 * * * * cd /var/www/your_domain && php wp-cron.php > /dev/null 2>&1

Step 10 - Final sync and DNS cutover

Content changed on the old site while you were testing. To avoid losing new orders, comments or posts, freeze changes on the old site (put it in maintenance mode or stop publishing), then run a final sync.

Copy changed files again (only differences are transferred):

rsync -az --info=progress2 cpanel_user@old_host:public_html/ /var/www/your_domain/ --exclude wp-config.php
sudo chown -R www-data:www-data /var/www/your_domain

The --exclude keeps your updated wp-config.php from being overwritten. Export the database again on the shared host and import it over the existing one on the VPS. The dump contains DROP TABLE statements, so it replaces the tables cleanly:

ssh cpanel_user@old_host
mysqldump --single-transaction --no-tablespaces -u cpanel_user_wpuser -p cpanel_user_wpdb > ~/site-final.sql
exit

Then, on the VPS:

scp cpanel_user@old_host:site-final.sql ~/site-final.sql
mysql -u wordpress -p wordpress < ~/site-final.sql

Now change the A records for your_domain and www at your DNS provider to your_server_ip. If you use IPv6, update the AAAA records as well, or remove them if the VPS will not serve IPv6. Leave the MX records unchanged for now.

Check which address public resolvers return:

dig +short your_domain @1.1.1.1
dig +short your_domain @8.8.8.8
your_server_ip

Within a few minutes (the TTL you set in Step 1), traffic arrives at the VPS. Confirm it in the access log:

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

If something goes wrong, point the A records back to the old IP. The old site is still intact.

Step 11 - Enabling HTTPS with Let's Encrypt

Once DNS points to the VPS, request a free certificate. Certbot's Nginx plugin edits the server block and sets up automatic renewal:

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

Confirm that renewal works:

sudo certbot renew --dry-run
Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/your_domain/fullchain.pem (success)

Open https://your_domain in a browser and check that the padlock shows a valid certificate.

Step 12 - Handling email

If your mailboxes live on the shared host, they stop working the day you cancel the account. Running your own mail server on a VPS is possible but demanding: IP reputation, SPF, DKIM, DMARC, spam filtering and backups all become your job. For most sites, the practical path is:

  1. Create the mailboxes at a dedicated email provider.
  2. Use the provider's migration tool (most offer IMAP import) to copy the existing messages from the shared host.
  3. Change the MX records and add the SPF and DKIM records the provider gives you.
  4. Keep the old mailboxes until the new provider receives mail normally for a few days.

If the site sends email (contact forms, password resets), configure it to send through an authenticated SMTP relay rather than PHP's mail() function, which has no mail server behind it on a fresh VPS.

Troubleshooting

Error establishing a database connection: the credentials in wp-config.php do not match the user you created, or the import went into another database. Test with mysql -u wordpress -p wordpress.

502 Bad Gateway: Nginx cannot reach PHP-FPM. Check that php8.3-fpm is running and that the socket path in fastcgi_pass matches ls /run/php/.

413 Request Entity Too Large on uploads: raise client_max_body_size in the server block and upload_max_filesize and post_max_size in /etc/php/8.3/fpm/php.ini, then reload both services.

Pages other than the home page return 404: the try_files line is missing or the application needs rewrite rules that were in .htaccess.

Conclusion

Your site now runs on a VPS you control, with the database and files copied consistently, DNS switched with a short TTL and HTTPS from Let's Encrypt. Keep the shared hosting account for a week or two as a fallback before cancelling it. Next steps: set up automated backups of /var/www and the database, raise the DNS TTL back to a normal value such as 3600, and enable unattended security upgrades on the VPS.