Back to tutorials
Tutorial

rsync migration tutorial (2026): Move a WordPress site from shared hosting to a VPS with a staged cutover

rsync migration tutorial for 2026: copy WordPress from shared hosting to a VPS safely, then cut over DNS with minimal risk.

By Anurag Singh
Updated on Sep 25, 2026
Category: Tutorial
Share article
rsync migration tutorial (2026): Move a WordPress site from shared hosting to a VPS with a staged cutover

Most WordPress “migrations” don’t fail because rsync is hard. They fail because small details get missed.

Common culprits include permissions drift, a partial file copy, or flipping DNS before you’ve proven the new server works.

This rsync migration tutorial avoids that. You’ll sync files in passes, import the database, and test the site behind a preview hostname.

Then you’ll run one short final sync and cut over with minimal drama.

What you’ll build (and what you’ll avoid)

You’ll end up with WordPress running on your VPS and serving the real domain over HTTPS.

You’ll also have a rollback plan that’s simple enough to run under pressure.

  • Staged rsync: one big copy now, then a quick delta sync right before cutover
  • Database import with a short, planned “content freeze” window
  • Safe preview using /etc/hosts and a temporary vhost
  • DNS cutover with low TTL and a clear, practiced rollback path

If you need a VPS for this workflow, a HostMyCode VPS fits well.

You get root access, consistent performance, and enough headroom to tune PHP and caching as the site grows.

Prerequisites checklist (10 minutes of prep)

Do these before you copy a single file.

They prevent two classic failures: syncing into the wrong directory and cutting DNS while TLS is still broken.

  • A fresh VPS running Ubuntu 24.04 LTS or Debian 12 (recommended for 2026)
  • SSH access to your shared hosting account (SSH preferred; SFTP works with more friction)
  • Your domain’s DNS access (registrar or DNS provider)
  • Basic stack on VPS: Nginx or Apache, PHP 8.3/8.4, and MariaDB/MySQL

Before cutover, lower DNS TTL.

If you haven’t done this before, follow this DNS cutover tutorial. Then come back once TTL is set to 300 seconds.

Step 1 — Create the target site on your VPS

Pick a layout and stick to it.

The example below uses Nginx + PHP-FPM, with the WordPress docroot at /var/www/example.com/public.

  1. Create directories:

    sudo mkdir -p /var/www/example.com/{public,logs}
    sudo chown -R www-data:www-data /var/www/example.com
  2. Install baseline packages (Ubuntu/Debian):

    sudo apt update
    sudo apt -y install nginx rsync unzip curl mariadb-client
  3. Make sure PHP-FPM is installed (package names vary by distro and PHP version):

    sudo apt -y install php-fpm php-mysql php-curl php-gd php-intl php-mbstring php-xml php-zip

If you want per-site resource controls (useful on multi-site VPS setups), tune pools later.

This guide pairs well with VPS PHP-FPM pool tuning.

Step 2 — Create a temporary “preview” hostname on the VPS

You need a way to test the VPS without touching public DNS.

Use a temporary server_name such as vps-preview.example.com.

Or keep it local-only via /etc/hosts.

Create an Nginx server block:

sudo nano /etc/nginx/sites-available/example.com
server {
  listen 80;
  server_name vps-preview.example.com;

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

  access_log /var/www/example.com/logs/access.log;
  error_log  /var/www/example.com/logs/error.log;

  client_max_body_size 128m;

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

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

Enable and reload:

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

Tip: Many distros use a versioned socket like /run/php/php8.3-fpm.sock. If yours does, update the config.

Confirm the correct path with:

ls -al /run/php/

Step 3 — Pull WordPress files from shared hosting with rsync (initial sync)

This is what makes the staged approach work.

You do one heavy copy now, then only ship changes later. That keeps the final downtime window small.

First, identify the WordPress docroot on shared hosting. Common paths:

  • cPanel: /home/USER/public_html
  • Add-on domain: /home/USER/public_html/subfolder

From your VPS, run an initial sync (replace values):

rsync -avz --progress \
  -e "ssh -p 22" \
  USER@shared-host.example:/home/USER/public_html/ \
  /var/www/example.com/public/

Add exclusions so you don’t haul over caches and old backup archives:

rsync -avz --progress \
  --exclude 'wp-content/cache/' \
  --exclude 'wp-content/uploads/cache/' \
  --exclude 'wp-content/updraft/' \
  --exclude '.well-known/' \
  -e "ssh -p 22" \
  USER@shared-host.example:/home/USER/public_html/ \
  /var/www/example.com/public/

Pitfall: Don’t use --delete on the first run.

Save it for the final delta sync, once you’re certain the destination path is correct.

Step 4 — Export and import the database

Shared hosting usually includes MySQL/MariaDB access.

Gather the database name, username, password, and host.

In cPanel, you’ll find them under “MySQL Databases.”

WordPress also stores them in wp-config.php.

On the shared host, create a dump. If you have SSH, run:

mysqldump --single-transaction --quick --routines --triggers \
  -u DBUSER -p DBNAME > /home/USER/db.sql

Copy it to the VPS:

rsync -avz -e "ssh -p 22" USER@shared-host.example:/home/USER/db.sql /root/

On the VPS, create a database and user:

sudo mysql
CREATE DATABASE wp_example DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_example'@'localhost' IDENTIFIED BY 'use-a-long-random-password';
GRANT ALL PRIVILEGES ON wp_example.* TO 'wp_example'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Import:

mysql -u wp_example -p wp_example < /root/db.sql

Step 5 — Fix wp-config.php and verify permissions

Update database settings in /var/www/example.com/public/wp-config.php:

define('DB_NAME', 'wp_example');
define('DB_USER', 'wp_example');
define('DB_PASSWORD', 'use-a-long-random-password');
define('DB_HOST', 'localhost');

Then normalize permissions.

Don’t “eyeball” this. Set it deliberately:

sudo chown -R www-data:www-data /var/www/example.com/public
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 plan to upload themes/plugins via wp-admin, make sure wp-content/uploads is writable:

sudo chmod -R 775 /var/www/example.com/public/wp-content/uploads

Step 6 — Preview the site safely (no public DNS changes yet)

You have two solid ways to preview.

Use whichever fits your situation.

  1. /etc/hosts method (recommended): on your laptop, map the preview hostname to the VPS IP temporarily.

    # Linux/macOS
    sudo nano /etc/hosts
    
    # Add:
    203.0.113.10   vps-preview.example.com
  2. Temporary DNS record: create an A record for vps-preview.example.com pointing to the VPS IP with TTL 300.

Browse the preview hostname and test the things that usually break first:

  • wp-admin login works
  • Permalinks resolve (Settings → Permalinks → Save)
  • Uploads load and new uploads work
  • Contact forms send email (verify; don’t assume)

If you hit HTTPS redirect loops during preview, you’ll fix it faster with this HTTPS redirect troubleshooting tutorial.

Step 7 — Set up HTTPS on the VPS (before cutover)

Issue a Let’s Encrypt certificate for the preview hostname first.

This validates your web server config and renewal path before you move the real domain.

Install Certbot for Nginx:

sudo apt -y install certbot python3-certbot-nginx

Request a cert (replace hostname):

sudo certbot --nginx -d vps-preview.example.com

If issuance or renewals fail, don’t keep retrying blindly.

Work through a targeted checklist from Let’s Encrypt renewal troubleshooting.

Step 8 — Plan the “final sync” window (content freeze) and run delta rsync

You only need downtime for writes.

That includes new orders, comments, uploads, and admin changes.

Pick a quiet window and treat the freeze like a real change event.

  • Put the site in maintenance mode, or temporarily disable new writes (store checkout, forms, membership posts).
  • Tell stakeholders the freeze start and end time.

Run a delta rsync from the VPS.

This is the run where --delete makes sense, because you want the destination to match the source:

rsync -avz --delete --progress \
  --exclude 'wp-content/cache/' \
  --exclude 'wp-content/uploads/cache/' \
  --exclude 'wp-content/updraft/' \
  --exclude '.well-known/' \
  -e "ssh -p 22" \
  USER@shared-host.example:/home/USER/public_html/ \
  /var/www/example.com/public/

Then create a fresh database dump and import again.

You can import only once if the site truly didn’t change.

For anything with real traffic, the second import is usually safer than hoping nothing wrote to the DB.

Step 9 — Switch the live domain on the VPS (vhost + WordPress URLs)

Update Nginx to answer for the real domain.

Edit the same server block and change:

server_name example.com www.example.com;

Reload:

sudo nginx -t
sudo systemctl reload nginx

In WordPress, siteurl and home must match the real domain.

If they don’t, you’ll see admin redirects, mixed content, or both.

Set them directly in the database:

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

If your table prefix isn’t wp_, adjust the query.

You can confirm the prefix in wp-config.php.

Step 10 — Issue the real SSL certificate and harden TLS basics

Once the domain points to your VPS (next step), request the certificate for example.com.

If DNS propagates quickly, you can run this a few minutes after updating the A record.

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

After it’s live, apply production TLS settings (ciphers, HSTS, OCSP stapling).

Keep it practical and stick to a checklist.

Use this TLS hardening tutorial as your follow-up.

Step 11 — Cut over DNS (A/AAAA), then validate from multiple networks

Update DNS records to the VPS IP:

  • A: example.com → VPS IPv4
  • A: www → VPS IPv4 (or CNAME to apex)
  • AAAA: update only if your VPS serves IPv6 correctly

After changing DNS, verify resolution and HTTP behavior:

# Replace with your domain
dig +short example.com A
curl -I https://example.com
curl -I https://www.example.com

Then test from a phone on mobile data and, ideally, another network.

DNS caches can mask problems. Real clients won’t.

If you get “wrong IP” or NXDOMAIN surprises, the fastest route is DNS propagation troubleshooting.

Step 12 — Post-cutover cleanup (the parts people skip)

This is where you prevent the “moved it yesterday, broke it today” tickets.

Set aside 30 minutes and close the loop.

  • Disable maintenance mode and place a test order / submit a form.
  • Check error logs: /var/www/example.com/logs/error.log and /var/log/nginx/error.log.
  • Confirm cron works. Missed WP-Cron is common after a move.
  • Set backups on the VPS before you delete anything on shared hosting.

If scheduled posts or WooCommerce emails stop, fix it cleanly using WordPress cron troubleshooting.

Common rsync migration mistakes (and quick fixes)

  • Media missing after cutover: you copied WordPress core but not wp-content/uploads, or permissions are wrong.

    Fix: re-run rsync and ensure uploads is writable by your web user.

  • Admin redirects to old domain: siteurl/home still set to old values or hard-coded in wp-config.php.

    Fix: update DB values and remove hard-coded definitions if present.

  • Permission denied during rsync: SSH user can’t read some paths, or you’re syncing from the wrong directory.

    Fix: sync from the actual docroot and avoid system paths.

    Verify with pwd and ls -al on shared hosting.

  • Slow import / timeouts: huge DB dump with low shared-host limits.

    Fix: compress the dump (gzip) and import on VPS, or split the dump.

Performance baseline after the move (simple wins)

A VPS gives you control, but it also removes excuses.

Do these three checks right after the site is stable.

  1. PHP-FPM status: confirm the PHP service is stable under load.

    sudo systemctl status php-fpm
  2. Page caching plan: for WordPress, server-side caching usually beats plugin-only caching.

    If you run Nginx, consider FastCGI cache with correct WooCommerce exclusions.

    Follow this WordPress full-page caching tutorial.

  3. Disk and RAM headroom: migrations often expose how close you were to the edge.

    df -h
    free -h

Summary: a staged rsync approach keeps you in control

The staged approach is the whole point.

Do the heavy rsync early, import the database, and validate on a preview hostname. Then run a short delta sync before DNS cutover.

You spend a little more time up front. In return, you get a calmer go-live with an obvious rollback.

If you want this migration pattern without babysitting every layer, start on managed VPS hosting from HostMyCode.

You keep control of your application, while a hosting team handles the server work that tends to break at the worst times.

If you’re moving off shared hosting because you’ve hit CPU limits, plugin conflicts, or unpredictable slowdowns, a VPS is a clean reset. Use a HostMyCode VPS for full root access, or choose managed VPS hosting if you want help with setup, updates, and hardening while you focus on the site.

FAQ

Can I run this rsync migration tutorial if my shared host only offers SFTP?

Yes, but rsync works best over SSH.

Some hosts don’t provide rsync on the shared server at all. If rsync isn’t available remotely, you can still copy with SFTP, but you lose fast delta syncing and quick verification.

Do I need to keep the old shared hosting account after cutover?

Keep it for at least 7–14 days.

That buffer covers late DNS caches, forgotten subdomains, and other “we didn’t realize this depended on the old server” surprises.

How do I handle email during the move?

If email is hosted on the shared account, don’t change MX records until you’ve planned the mail migration.

If you’re moving mail too, verify SPF/DKIM/rDNS so you don’t take a deliverability hit.

What’s the safest rollback plan?

Rollback is DNS: point A records back to the old server, then re-enable the old site.

Keep TTL low during the change window so rollback happens quickly.

Should I use IPv6 (AAAA) on day one?

Only if the VPS firewall and web server are configured for IPv6.

A broken AAAA record creates intermittent failures for IPv6-capable clients.

rsync migration tutorial (2026): Move a WordPress site from shared hosting to a VPS with a staged cutover | HostMyCode