Back to tutorials
Tutorial

Migrate WordPress to a VPS With rsync and MariaDB Dump

Migrate WordPress to a VPS safely with rsync + MariaDB dump, plus permissions fixes, SSL, and cutover verification steps.

By Anurag Singh
Updated on Oct 19, 2026
Category: Tutorial
Share article
Migrate WordPress to a VPS With rsync and MariaDB Dump

You can migrate WordPress to a VPS without plugins, without “mystery files,” and without a stressful cutover. This workflow uses two boring, reliable tools:

  • rsync for the WordPress filesystem
  • a MariaDB/MySQL dump for the database

The goal is simple. You want the same content and URLs on your VPS.

You also want a cutover you can reverse quickly if anything looks wrong.

What you’ll build (and what you need)

By the end, WordPress will run on your VPS (Ubuntu/Debian/AlmaLinux/Rocky are all fine) behind Nginx or Apache.

You’ll import the database, apply sane permissions, and run a short checklist before you flip DNS.

  • Source: Shared hosting (cPanel or similar). SSH access is ideal. If you only have SFTP, you can still do most of this. You just lose rsync’s speed and incremental sync.
  • Destination: A fresh VPS with root SSH access.
  • Time: 45–120 minutes, mostly depending on your media library and upstream bandwidth.

If you haven’t picked a server yet, a small HostMyCode VPS is a typical starting point for a single WordPress site.

If you’d rather not babysit patching and troubleshooting during the move, managed VPS hosting is the lower-stress route.

Plan the migration so you can roll back

A clean move usually comes down to two passes:

  1. Baseline sync while the old site stays live (copies the bulk of files).
  2. Short maintenance window on the old host, then a final rsync plus a fresh database dump.

That maintenance window matters. It freezes writes.

With writes paused, your final file copy and database export match.

Before you touch anything: lower your DNS TTL for the domain to 300 seconds (5 minutes) a few hours ahead of time.

A low TTL makes both cutover and rollback faster.

If you want an end-to-end DNS runbook, follow this DNS cutover tutorial. It pairs well with the rsync + dump workflow.

Step 1: Prep the VPS (web server, PHP, MariaDB)

This tutorial assumes Nginx + PHP-FPM + MariaDB on Ubuntu/Debian.

Where AlmaLinux/Rocky differs, the commands are shown.

Create DNS records later, but set your hostname now

Give the VPS a real hostname. You’ll see it in logs.

TLS tooling is also easier later.

sudo hostnamectl set-hostname vps1.example.com
hostnamectl

Install packages (Ubuntu/Debian)

sudo apt update
sudo apt -y install nginx mariadb-server php-fpm php-mysql php-cli php-curl php-gd php-xml php-mbstring php-zip unzip rsync

Confirm versions:

nginx -v
php -v
mariadb --version

Install packages (AlmaLinux/Rocky)

sudo dnf -y install nginx mariadb-server php-fpm php-mysqlnd php-cli php-curl php-gd php-xml php-mbstring php-zip unzip rsync

Enable and start services:

sudo systemctl enable --now nginx mariadb php-fpm
sudo systemctl status nginx mariadb php-fpm --no-pager

Create the WordPress database and user

Pick a database name and user. Example:

  • DB: wp_example
  • User: wp_user

Create them on the VPS:

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

Verification:

mysql -u wp_user -p -e "SHOW DATABASES;"

Create the web root on the VPS

Use a predictable layout. Example for a single site:

sudo mkdir -p /var/www/example.com/public
sudo chown -R www-data:www-data /var/www/example.com

On AlmaLinux/Rocky, the web user is usually nginx or apache.

Confirm it with:

ps aux | egrep 'nginx: worker|httpd' | head

Step 2: Get shared-host credentials and paths (source server)

From the old host, collect:

  • SSH/SFTP login (username, host/IP, port)
  • The WordPress path (often public_html)
  • Database credentials from wp-config.php (DB name/user/password/host)

On the source host, pull the database settings:

cd ~/public_html
sed -n '1,120p' wp-config.php | egrep "DB_NAME|DB_USER|DB_PASSWORD|DB_HOST"

Common mistake: syncing the wrong directory.

Shared accounts often host multiple installs.

Make sure you’re in the folder that contains wp-content/, wp-admin/, and wp-includes/.

Step 3: Baseline rsync (copy WordPress files while live)

Run rsync from the VPS and pull from the old host.

This keeps the “source of truth” on the old server. It also avoids re-uploading large media libraries.

Example (source is cPanel-style public_html):

sudo rsync -avz --progress \
  -e "ssh -p 22" \
  olduser@oldhost.example.net:/home/olduser/public_html/ \
  /var/www/example.com/public/

If SSH uses a custom port, change it.

If you have an SSH key, add -i:

sudo rsync -avz --progress \
  -e "ssh -i /root/.ssh/id_ed25519 -p 2222" \
  olduser@oldhost.example.net:/home/olduser/public_html/ \
  /var/www/example.com/public/

Verification:

ls -la /var/www/example.com/public | head
test -f /var/www/example.com/public/wp-config.php && echo "wp-config.php present"

Speed tweak for big media libraries

For huge libraries, compression can help if bandwidth is your bottleneck.

If the shared host CPU is weak, compression can slow the transfer. In that case, drop -z:

sudo rsync -av --progress \
  -e "ssh -p 22" \
  olduser@oldhost.example.net:/home/olduser/public_html/ \
  /var/www/example.com/public/

Step 4: Put the old site into maintenance mode for the final sync

For a consistent final copy, pause writes on the old site.

Keep this window short, but do it.

If you have WP-CLI on the old host:

cd ~/public_html
wp maintenance-mode activate

If you don’t have WP-CLI, create .maintenance (WordPress will detect it):

cd ~/public_html
php -r 'file_put_contents(".maintenance","<?php $upgrading = time(); ?>");'

Verification: load the site in a browser. You should see a maintenance message.

Step 5: Export the MariaDB/MySQL database (source)

On the old host, run mysqldump using the DB values from wp-config.php.

cd ~
mysqldump \
  --single-transaction \
  --quick \
  --default-character-set=utf8mb4 \
  -h DB_HOST -u DB_USER -p DB_NAME \
  > wp.sql

Compress it to speed transfer:

gzip -1 wp.sql

Verification:

ls -lh wp.sql.gz
zcat wp.sql.gz | head

You should see SQL such as CREATE TABLE and INSERT.

Step 6: Transfer the dump to the VPS and import it

Copy the dump to the VPS. From the VPS:

sudo rsync -avz \
  -e "ssh -p 22" \
  olduser@oldhost.example.net:/home/olduser/wp.sql.gz \
  /root/

Import into the database you created:

gunzip -c /root/wp.sql.gz | mysql -u wp_user -p wp_example

Verification: check table count:

mysql -u wp_user -p -D wp_example -e "SHOW TABLES;" | wc -l

If you see only 0–1 lines, you imported into the wrong database.

The dump could also be empty.

Step 7: Update wp-config.php on the VPS

Point WordPress at the new local database:

sudo nano /var/www/example.com/public/wp-config.php

Set these values to match the VPS database:

define('DB_NAME', 'wp_example');
define('DB_USER', 'wp_user');
define('DB_PASSWORD', 'REPLACE_WITH_A_LONG_RANDOM_PASSWORD');
define('DB_HOST', 'localhost');

Common mistake: leaving DB_HOST set to a remote hostname from shared hosting.

On a typical VPS install, it should be localhost.

Step 8: Fix ownership and sane file permissions

Migrations often drag along shared-host ownership and overly loose permissions.

On the VPS, aim for “readable by the web server, writable only where needed.”

Assuming Nginx/Apache runs as www-data (Ubuntu/Debian):

sudo chown -R www-data:www-data /var/www/example.com/public

Set directories to 755 and files to 644:

sudo find /var/www/example.com/public -type d -exec chmod 755 {} \;
sudo find /var/www/example.com/public -type f -exec chmod 644 {} \;

If you use automatic updates, WordPress needs write access to:

  • wp-content/uploads
  • wp-content/cache (if your cache plugin writes here)
sudo mkdir -p /var/www/example.com/public/wp-content/{uploads,cache}
sudo chown -R www-data:www-data /var/www/example.com/public/wp-content

Step 9: Configure Nginx for the WordPress site

Create an Nginx server block. On Ubuntu/Debian:

sudo nano /etc/nginx/sites-available/example.com

Paste this (adjust server_name and PHP socket path if needed):

server {
    listen 80;
    server_name example.com www.example.com;

    root /var/www/example.com/public;
    index index.php index.html;

    access_log /var/log/nginx/example.com.access.log;
    error_log  /var/log/nginx/example.com.error.log;

    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 ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff2?)$ {
        expires 7d;
        add_header Cache-Control "public, max-age=604800";
    }

    location = /xmlrpc.php {
        deny all;
    }
}

If your PHP-FPM socket is different, find it:

ls /run/php/

Enable the site and reload:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo nginx -t
sudo systemctl reload nginx

Verification:

curl -I http://127.0.0.1/

You should get HTTP/1.1 200 or a redirect. That depends on your WordPress settings.

Step 10: Preview the VPS site before DNS cutover (hosts file method)

Don’t change DNS until you’ve browsed the VPS copy.

The simplest preview is a hosts file override on your laptop.

Find your VPS public IP, then add an entry on your local machine:

  • Linux/macOS: /etc/hosts
  • Windows: C:\Windows\System32\drivers\etc\hosts
VPS_IP_ADDRESS example.com www.example.com

Now open http://example.com in a browser. You should see the VPS site.

Common mistake: testing from the server itself and calling it “good.”

Always test from a client browser using the real domain, because WordPress keyes off hostnames.

Step 11: Fix WordPress URLs if the domain or scheme changes

If the domain stays the same and you’re keeping the same site URL, you may not need to touch anything.

If you switched http to https or changed domains, update URLs carefully.

First, check what WordPress currently has:

mysql -u wp_user -p -D wp_example -e "SELECT option_name, option_value FROM wp_options WHERE option_name IN ('siteurl','home');"

If you need to set them (example: switching to HTTPS):

mysql -u wp_user -p -D wp_example -e "UPDATE wp_options SET option_value='https://example.com' WHERE option_name IN ('siteurl','home');"

If you also changed domains, you’ll usually need a serialized-safe search/replace.

WP-CLI is the safest option.

Install WP-CLI on the VPS:

curl -L -o /usr/local/bin/wp https://github.com/wp-cli/wp-cli/releases/latest/download/wp-cli.phar
sudo chmod +x /usr/local/bin/wp
wp --info

Run the replacement from the WordPress directory:

cd /var/www/example.com/public
sudo -u www-data wp search-replace 'http://old-domain.example' 'https://example.com' --all-tables --precise --report-changed-only

Verification: open a few posts/pages, confirm images load, and check the page source for old-domain references.

Step 12: Final rsync pass (after DB dump) to capture late file changes

While the old site is still in maintenance mode, run rsync again.

This pass is usually fast. It catches last-minute media uploads and plugin/theme changes.

sudo rsync -avz --delete --progress \
  -e "ssh -p 22" \
  olduser@oldhost.example.net:/home/olduser/public_html/ \
  /var/www/example.com/public/

Why --delete matters: it removes files on the VPS that were deleted on the source.

That helps avoid stale plugin/theme leftovers.

Common mistake: using --delete against the wrong destination.

Double-check the path and trailing slashes before you hit enter.

Step 13: Add TLS (Let’s Encrypt) on the VPS

Once DNS points at the VPS, turn on HTTPS.

If you try HTTP validation before cutover, it can fail unless you use DNS validation.

Install Certbot for Nginx on Ubuntu/Debian:

sudo apt -y install certbot python3-certbot-nginx

After you cut DNS (next step), run:

sudo certbot --nginx -d example.com -d www.example.com

If you want a deeper HTTPS troubleshooting path (mixed content, renewals, redirect loops), use HostMyCode’s TLS setup guide.

Step 14: DNS cutover with fast rollback

Before you touch DNS, you should have:

  • VPS site working via hosts file preview
  • Database imported and URLs correct
  • Old site still in maintenance mode (so the cutover stays clean)

Update your DNS A record(s) to the VPS IP.

If your domain is registered elsewhere, manage DNS there or move it to HostMyCode domains so DNS and renewals sit in one place.

Verification commands from your laptop:

dig +short example.com
dig +short www.example.com

Once those return the VPS IP, load the site in an incognito window.

You should land on the VPS.

Step 15: Turn off maintenance mode and run post-migration checks

Keep maintenance enabled on the old host until you’re sure traffic is hitting the VPS.

Then disable it.

Old host (WP-CLI):

cd ~/public_html
wp maintenance-mode deactivate

Or remove the file:

rm -f ~/public_html/.maintenance

Quick health checklist on the VPS

  • Homepage loads and shows current content
  • Admin login works at /wp-admin
  • Permalinks work (try a blog post URL)
  • Media loads (open a few images, check wp-content/uploads)
  • Forms work (contact form, checkout, password reset)

Log checks:

sudo tail -n 80 /var/log/nginx/example.com.error.log
sudo tail -n 80 /var/log/nginx/example.com.access.log

If you see repeated 404 for /wp-admin/admin-ajax.php or theme assets, your rsync source path is wrong.

A plugin may also expect files outside the WordPress root.

Step 16: Basic hardening (don’t leave the VPS wide open)

On shared hosting, the platform hides a lot of security defaults.

On a VPS, those defaults are yours to set.

  • Enable a firewall (allow SSH, HTTP, HTTPS).
  • Lock down WordPress admin basics (file permissions, disable XML-RPC if unused).
  • Keep the OS and plugins patched.

If you’re on Ubuntu and want a clean baseline, follow this UFW firewall tutorial.

For WordPress-specific hardening, use this WordPress hardening guide.

Step 17: Clean up, backups, and a restore you’ve actually tested

Right after the move, handle two unglamorous tasks:

  1. Delete leftover database dumps (they contain site data and can include sensitive content).
  2. Set up backups and run a restore test while the details are still fresh.
sudo shred -u /root/wp.sql.gz 2>/dev/null || sudo rm -f /root/wp.sql.gz

For a realistic, automated restore test (WordPress files + database), follow this backup verification tutorial.

It’s the difference between “we have backups” and “we can recover on demand.”

Common migration failures (and quick fixes)

  • White screen / PHP fatal error: check /var/log/nginx/example.com.error.log and your PHP-FPM version/socket path. Disable a bad plugin by renaming its folder under wp-content/plugins.
  • Redirect loop after enabling HTTPS: there’s often a mismatch between WordPress siteurl/home, forced HTTPS settings, and reverse proxy headers. Start by setting siteurl and home to the exact scheme + domain.
  • Images missing: you rsynced the wrong directory or missed wp-content/uploads. Re-run rsync and confirm the path.
  • 403 Forbidden: wrong ownership/permissions. Re-apply chown and sane modes (755 dirs, 644 files).
  • Database import fails with packet errors: increase max_allowed_packet in MariaDB and re-import, or import from an uncompressed dump file.

Summary: the clean, repeatable way to migrate

To migrate WordPress to a VPS cleanly, keep it boring.

Do a baseline rsync, take a short maintenance window, export a fresh database dump, then run a final rsync with --delete.

Validate using a hosts override before DNS cutover.

After traffic lands on the VPS, add TLS, tighten security basics, and set up backups with a restore you’ve actually run.

If you want the move to be about the site—not about server firefighting—use managed VPS hosting.

If you prefer full control (and you’re comfortable with SSH), start with a HostMyCode VPS and scale resources as traffic grows.

Moving off shared hosting is a good moment to standardize your stack and get backups right. HostMyCode makes it easy to start with a right-sized VPS, then switch to managed VPS hosting if you want help with updates, security, and migrations.

FAQ: rsync + MariaDB dump WordPress migrations

Do I need to install WordPress fresh on the VPS first?

No. With this approach, you copy the existing WordPress codebase and wp-content, then import the database.

The VPS only needs a working web stack (Nginx/Apache, PHP, MariaDB).

Can I do this without SSH on shared hosting?

You can, but it’s slower.

Without SSH, you’ll likely export a database via phpMyAdmin and transfer files via SFTP/FTP.

The steps are similar, but you lose rsync’s incremental speed and the clean final sync.

Should I copy everything under public_html?

Usually yes, but watch for extra folders that don’t belong to the site you’re migrating.

If the account hosts multiple sites, rsync only the correct document root for the domain.

How do I confirm I’m hitting the VPS after DNS cutover?

Check dig +short from your laptop, then watch the VPS access log while you load the site:

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

What about email on the old shared host?

If your domain’s MX records still point to the old provider, mail will keep landing there even after the website moves.

Plan email migration separately, and don’t change MX records during the same cutover window unless you’ve tested deliverability.