Skip to main content

Following this tutorial and don't have a VPS yet? Our Linux VPS start at €2.99/month with support included.

See our VPS

Migrate a WordPress Site from Shared Hosting to a VPS

Your WordPress site has grown. The shared hosting plan that was fine at launch is now showing its limits: erratic response times at peak hours, no way to install Redis or pick your PHP version, a process limit that takes the site down the moment a campaign gets a little traction. Moving to a VPS solves all of that, provided the migration is done properly.

This guide covers the complete, zero-visible-downtime migration of a WordPress site from any shared host (cPanel, Plesk, o2switch, OVH, Ionos, Hostinger, and so on) to a Debian 12 VPS running Nginx, PHP-FPM and MariaDB. The key principle: build the new server entirely, test it with the real site, and only switch DNS at the very end.

Do You Really Need a VPS?

Let's be honest: a VPS requires administration. System updates, security, backups: you handle them. In exchange, you get guaranteed resources and full control.

Shared hostingVPS
ResourcesShared with dozens of sitesGuaranteed (CPU, RAM, disk)
StackImposed (Apache, PHP as provided)Your choice (Nginx, PHP 8.3, Redis, server-side cache…)
AdministrationHandled by the hostYour responsibility
EmailUsually includedNeeds a separate solution
Best forBrochure site, small blogHigh-traffic sites, WooCommerce, multisite, agencies
Want the performance without the administration?

If what's driving you away from your current host is slow performance or poor support rather than a need for technical control, YorkHost cPanel Web Hosting may be the better answer: free SSL, Anti-DDoS included, professional email, and no server to administer. This guide is still the right one for anyone who wants full control of a VPS.

Prerequisites

  • A YorkHost Linux VPS running Debian 12, with root access. For a standard WordPress site, a VPS-4 (2 vCores, 4 GB) is enough; for WooCommerce or a high-traffic site, go for a VPS-8 at minimum.
  • Access to your current hosting: control panel (cPanel, Plesk, etc.), FTP/SFTP and phpMyAdmin
  • Access to your domain's DNS zone (at your registrar or your host)
  • SSH set up and secured on the VPS, with UFW active and ports 22, 80 and 443 open

Step 1: Audit the Existing Site

Before touching anything, write down what you are migrating. Five minutes here will save you hours of debugging later.

In the WordPress admin, under Tools → Site Health → Info, note down:

  • The PHP version in use (you will install the same one, or a newer compatible one)
  • The WordPress version and the size of the wp-content/uploads directory
  • The table prefix and the database name (visible in wp-config.php)
  • The list of active plugins, especially caching plugins (WP Rocket, LiteSpeed Cache, W3 Total Cache) and security plugins (Wordfence, iThemes), which often carry host-specific settings

Also check that the site doesn't rely on host-specific features: a "real" cron configured in the panel, scheduled tasks, redirects in .htaccess (Nginx doesn't use .htaccess, so we will rewrite them), or a specific SSL certificate.

Email

Most shared hosting plans include mailboxes. A VPS does not replace them: running your own mail server is a job in itself. Before cancelling your old hosting, decide where your email addresses will live (your registrar's email offer, the professional email included with YorkHost web hosting, Google Workspace, etc.). For emails sent by WordPress (notifications, WooCommerce orders), use an SMTP plugin (WP Mail SMTP, FluentSMTP) with a proper sending service rather than PHP's mail(), which ends up in spam.

Step 2: Export the Site

Two things to retrieve: the files and the database.

The Files

From your control panel's file manager or via FTP, download the entire site directory (usually public_html/ or www/). On cPanel, Backup → Download a Home Directory Backup produces a .tar.gz archive that is far faster to transfer than thousands of individual files over FTP.

If your host provides SSH access, it's even simpler: everything can be done with a single command from the VPS (see step 4).

The Database

Via phpMyAdmin: select the database, go to the Export tab, choose the Custom method and SQL format, tick Add DROP TABLE statement, and save the file. For a database of several hundred MB, enable gzip compression.

If you have SSH on the old hosting:

mysqldump -u DB_USER -p --single-transaction --default-character-set=utf8mb4 DB_NAME | gzip > wordpress.sql.gz
What about migration plugins?

Duplicator, All-in-One WP Migration and UpdraftPlus work very well and automate part of this guide. Their limitation: the free versions often cap the size (512 MB for All-in-One), and they hide what is going on, which makes diagnosis harder when something breaks. The manual method described here works for a 50 MB site as well as a 50 GB one, and you will understand your server by the end.

Step 3: Prepare the VPS

Install the Stack

sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx mariadb-server php-fpm php-mysql php-curl php-gd php-intl php-mbstring php-xml php-zip php-imagick php-bcmath unzip rsync

On Debian 12, this installs PHP 8.2, which is compatible with all recent WordPress versions. If you need PHP 8.3 or 8.4, add the Sury repository:

sudo apt install -y apt-transport-https lsb-release ca-certificates curl
curl -fsSLo /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg
echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/php.list
sudo apt update
sudo apt install -y php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd php8.3-intl php8.3-mbstring php8.3-xml php8.3-zip php8.3-imagick php8.3-bcmath

In the rest of this guide, replace php8.2-fpm with the version you installed.

Secure MariaDB and Create the Database

sudo mysql_secure_installation

Answer yes to everything (root password, removal of anonymous users and the test database). Then create the database and user, reusing exactly the same database name and table prefix as on the old hosting so nothing needs to change in wp-config.php:

sudo mysql
CREATE DATABASE monsite_wp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'monsite'@'localhost' IDENTIFIED BY 'un-mot-de-passe-fort';
GRANT ALL PRIVILEGES ON monsite_wp.* TO 'monsite'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Tune PHP-FPM for WordPress

The defaults are too low for a site with media:

sudo nano /etc/php/8.2/fpm/php.ini
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 120
max_input_vars = 3000

And size the pool according to the VPS RAM (each PHP process uses 30 to 60 MB):

sudo nano /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 20 # VPS-4: ~12, VPS-8: ~20, VPS-16: ~40
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
sudo systemctl restart php8.2-fpm

Install WP-CLI

WP-CLI is the WordPress command-line tool. It will make the rest of the process much faster:

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

Step 4: Transfer the Files

Create the site directory and hand it over to the Nginx/PHP user:

sudo mkdir -p /var/www/monsite.fr
sudo chown -R www-data:www-data /var/www/monsite.fr

Option A: rsync from the Old Hosting (if you have SSH)

This is the most reliable method: it resumes where it left off if the transfer drops, and you can run it again to sync the latest changes right before the switch.

sudo rsync -avz --progress -e "ssh -p 22" ancien_user@ancien-hebergeur.fr:~/public_html/ /var/www/monsite.fr/

Option B: Upload the Archive

From your computer, send the archive you downloaded in step 2:

scp backup-monsite.tar.gz root@VPS_IP:/tmp/

Then on the VPS:

cd /tmp && tar -xzf backup-monsite.tar.gz
# Adjust the path to match the archive structure (often homedir/public_html)
sudo rsync -a /tmp/homedir/public_html/ /var/www/monsite.fr/

You can also use a graphical SFTP client, see SFTP with VSCode.

Permissions

sudo chown -R www-data:www-data /var/www/monsite.fr
sudo find /var/www/monsite.fr -type d -exec chmod 755 {} \;
sudo find /var/www/monsite.fr -type f -exec chmod 644 {} \;
sudo chmod 600 /var/www/monsite.fr/wp-config.php

Step 5: Import the Database

Send the dump to the VPS, then import it:

scp wordpress.sql.gz root@VPS_IP:/tmp/
gunzip < /tmp/wordpress.sql.gz | sudo mysql monsite_wp

Check that the tables are there:

sudo mysql -e "USE monsite_wp; SHOW TABLES;" | head

Update wp-config.php

Open /var/www/monsite.fr/wp-config.php and check these lines. If you reused the same names, only the password changes:

define( 'DB_NAME', 'monsite_wp' );
define( 'DB_USER', 'monsite' );
define( 'DB_PASSWORD', 'un-mot-de-passe-fort' );
define( 'DB_HOST', 'localhost' );

While you're there, add these lines if they are missing:

define( 'WP_MEMORY_LIMIT', '256M' );
define( 'DISALLOW_FILE_EDIT', true ); // disables the file editor in the admin
define( 'FS_METHOD', 'direct' );

Check that WordPress can talk to the database:

cd /var/www/monsite.fr
sudo -u www-data wp db check
sudo -u www-data wp option get siteurl

Step 6: Configure Nginx

Nginx does not read .htaccess. Here is a complete, battle-tested configuration for WordPress:

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

root /var/www/monsite.fr;
index index.php index.html;

client_max_body_size 64M;

# Logs
access_log /var/log/nginx/monsite.fr.access.log;
error_log /var/log/nginx/monsite.fr.error.log;

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

# PHP via PHP-FPM
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 120;
}

# Browser caching for static files
location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico|css|js|woff2?|ttf)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}

# Security: block PHP execution in uploads and access to sensitive files
location ~* /wp-content/uploads/.*\.php$ { deny all; }
location ~ /\.(?!well-known) { deny all; }
location = /xmlrpc.php { deny all; }
location ~* (readme\.html|license\.txt|wp-config\.php) { deny all; }
}

Enable the site:

sudo ln -s /etc/nginx/sites-available/monsite.fr /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
Redirects inherited from .htaccess

If your old .htaccess contained custom redirects (Redirect 301 /old-page /new-page), convert them into Nginx blocks inside the server block: location = /old-page { return 301 /new-page; }. The standard WordPress rules are already covered by try_files.

Step 7: Test Before Switching DNS

This is the step that makes the migration risk-free. You are going to make your computer alone believe that monsite.fr already points to the VPS, while the rest of the world keeps seeing the old hosting.

Edit your machine's hosts file:

  • Windows: C:\Windows\System32\drivers\etc\hosts (open Notepad as administrator)
  • macOS / Linux: sudo nano /etc/hosts

Add:

VPS_IP   monsite.fr www.monsite.fr

Open http://monsite.fr in a private browsing window. You should see your site served by the VPS (check with the developer tools: the Server: nginx header instead of Apache or LiteSpeed). Browse the pages, log in to the admin, upload an image, test a contact form, place a test order if it's WooCommerce.

Any problems get fixed now, calmly, with zero impact on your visitors.

Changing domain name at the same time?

If the site is also changing address (for example monsite.ancien-hebergeur.frmonsite.fr), replace the old URL everywhere in the database with WP-CLI, which handles serialized data correctly, unlike a raw SQL search-and-replace:

cd /var/www/monsite.fr
sudo -u www-data wp search-replace 'https://ancien-domaine.fr' 'https://monsite.fr' --all-tables --precise

Add --dry-run first to see what would be changed.

Step 8: Switch DNS

Once the site is validated on the VPS:

  1. Lower the TTL of your DNS records to 300 seconds, ideally 24 hours before the switch, so the change propagates quickly. If you haven't done it, it's not a blocker; propagation will simply take longer.
  2. If you have SSH on the old hosting, do a final sync of the files and database (posts published and orders placed in the meantime): run the rsync again and re-import a fresh dump.
  3. Update the records:
TypeNameValueTTL
A@VPS_IP300
AwwwVPS_IP300
  1. Remove the line you added to your hosts file.
  2. Check propagation with dig monsite.fr +short or dnschecker.org.
Don't cancel the old hosting right away

Keep it active for at least two weeks. That covers stubborn DNS caches and lets you retrieve any forgotten file. Put the old site in maintenance or read-only mode during that time so no content gets created there by mistake.

Step 9: Enable HTTPS

As soon as the domain points to the VPS, get a Let's Encrypt certificate:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d monsite.fr -d www.monsite.fr

Choose the automatic HTTP → HTTPS redirect. Certbot edits the Nginx configuration and installs an automatic renewal timer. Verify it:

sudo certbot renew --dry-run

Then update the WordPress URLs if they were still on http://:

cd /var/www/monsite.fr
sudo -u www-data wp option update home 'https://monsite.fr'
sudo -u www-data wp option update siteurl 'https://monsite.fr'
sudo -u www-data wp search-replace 'http://monsite.fr' 'https://monsite.fr' --all-tables --precise

Step 10: Finishing Touches for a Fast, Secure Site

Reliable WordPress Cron

By default, WordPress runs its scheduled tasks on each visit, which is unreliable and costly. Replace that mechanism with a real cron:

// wp-config.php
define( 'DISABLE_WP_CRON', true );
sudo crontab -u www-data -e
*/5 * * * * cd /var/www/monsite.fr && /usr/local/bin/wp cron event run --due-now --quiet

See the Cron jobs guide for details.

Object Cache with Redis

An object cache avoids querying MariaDB again for the same data on every page. Typical gain: 30 to 50% less generation time in the admin and on WooCommerce.

sudo apt install -y redis-server php8.2-redis
sudo systemctl enable --now redis-server
sudo systemctl restart php8.2-fpm

Then install the Redis Object Cache plugin in WordPress and enable it.

Page Cache

For logged-out visitors, a page cache serves static HTML without running PHP. Two effective options: a plugin (WP Super Cache, Cache Enabler, or WP Rocket if you're willing to pay), or Nginx's FastCGI cache, which performs better but is a bit more technical. If you were already using a caching plugin on shared hosting, re-enable it and purge everything.

OPcache

Check that OPcache is enabled (it is by default on Debian) and increase its memory for WordPress:

sudo nano /etc/php/8.2/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=192
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=60

Protect the Login Page

Brute-force login attempts on wp-login.php are a constant on any public WordPress site. A dedicated Fail2ban jail blocks them at the firewall level. The full configuration is in Advanced Fail2ban. Also add two-factor authentication (Two Factor plugin or Wordfence).

Backups

Your YorkHost VPS includes 7-day rolling backups of the full disk, restorable in one click from the client area: a valuable safety net after an update goes wrong. Complement it with an off-site application backup (files + database dump): that is the subject of the Automatic offsite backup with Restic or Borg guide.

Migration Checklist

  • Audit: PHP version, plugins, uploads size, cron, redirects
  • Email solution decided before cancellation
  • Files and database exported
  • Stack installed, PHP-FPM sized, database created with the same name
  • Files transferred, permissions correct
  • Database imported, wp db check OK
  • Nginx configured, .htaccess redirects converted
  • Site tested via the hosts file: browsing, admin, upload, forms
  • TTL lowered, final sync, DNS switched
  • HTTPS active, URLs on https://
  • Real cron, Redis, page cache, Fail2ban, off-site backups
  • Old hosting kept for two weeks, then cancelled

Troubleshooting

Blank Page or 500 Error

Check /var/log/nginx/monsite.fr.error.log and /var/log/php8.2-fpm.log. Temporarily enable define( 'WP_DEBUG', true ); and define( 'WP_DEBUG_LOG', true ); in wp-config.php: the error will show up in wp-content/debug.log. Common cause: a missing PHP extension (php-intl, php-imagick) used by a theme.

"Error establishing a database connection"

The credentials in wp-config.php don't match those created in MariaDB, or DB_HOST points to the old host's server instead of localhost.

The location / { try_files ... } block is missing, or the active site in Nginx isn't the right one. sudo nginx -T | grep server_name lists the loaded vhosts. Then re-save the permalinks: Settings → Permalinks → Save Changes.

Infinite Redirect Loop

Usually a caching or SSL plugin (Really Simple SSL) configured for the old host, or an http/https mismatch between home, siteurl and the Nginx configuration. Deactivate plugins with wp plugin deactivate --all, fix the URLs, then re-enable them one by one.

Images Display but Uploads Fail

Permissions: sudo chown -R www-data:www-data /var/www/monsite.fr/wp-content/uploads. Or upload_max_filesize / client_max_body_size is too low.

The Site Is Slow After Migration

Before blaming the server: run htop to see whether PHP or MariaDB is saturated, then see Performance debugging. Nine times out of ten, it's a missing page cache or a plugin calling an external service on every page.

A VPS built for WordPress

Our Linux VPS run on Enterprise SSDs with Xeon processors clocked above 3 GHz, which matters for PHP since its execution is essentially single-threaded. Multi-Tbps Anti-DDoS is included at no extra cost, as are 7-day backups, and our French support team responds in under 2 hours during the day if you get stuck on any step of this migration.