Back to tutorials
Tutorial

WordPress Migration Tutorial (2026): Move a Site to a New VPS with Near-Zero Downtime

WordPress migration tutorial for 2026: copy files + DB, cut DNS safely, validate SSL, and roll back cleanly on a VPS.

By Anurag Singh
Updated on Jul 30, 2026
Category: Tutorial
Share article
WordPress Migration Tutorial (2026): Move a Site to a New VPS with Near-Zero Downtime

A WordPress migration tutorial should cover two non-negotiables. First, you need to keep the site writable for most of the move. Second, you need a rollback you can execute quickly.

This guide uses a “parallel run” approach. You build the new VPS alongside the old one.

Then you sync WordPress until you’re confident, test with a temporary mapping, and cut DNS with a short TTL and a predictable checklist.

The commands below assume Ubuntu 24.04 LTS (a common default in 2026 hosting). The same workflow works on Debian 12/13 or AlmaLinux/Rocky, with small package-name differences.

What you’ll build (and why this migration method works)

Skip the “export a ZIP and hope” approach. Instead, stage a real target server and sync it until cutover.

The goal is repeatability, not a one-off rescue.

  • Create a production-ready target VPS (web server, PHP, database, firewall).
  • Copy WordPress files with rsync (fast, resumable).
  • Copy the database with a consistent dump (or transaction-safe export).
  • Test using a temporary host mapping or a staging hostname.
  • Cut DNS with a low TTL and verify SSL, redirects, and forms.
  • Keep the old server intact for quick rollback.

If you’d rather not babysit the OS and web stack during a move, managed VPS hosting from HostMyCode fits this approach well. You keep control of the setup. You also get backup when something goes sideways at the worst possible hour.

Prerequisites and a quick pre-migration checklist

Get these items in hand before you touch DNS. If you’re missing even one, migrations turn into late-night “where is that setting?” hunts.

  • Current WordPress admin access and SSH/SFTP access to the source host.
  • Database credentials (DB name/user/host) from wp-config.php.
  • Current DNS provider access (A/AAAA records, CNAMEs, MX, TXT).
  • Disk usage: du -sh of public_html (or docroot) and DB size.
  • PHP version on source. Match it initially, then upgrade after the move.

Lower DNS TTL before cutover

6–24 hours before migration day, drop the TTL on your main A/AAAA records (and www) to 60–300 seconds. This keeps the cutover window predictable.

If your DNS host won’t allow a low TTL, plan for longer propagation. Otherwise, you’ll get more “why do I still see the old server?” confusion.

If you manage domains and DNS in one place, using HostMyCode Domains makes cutovers simpler. You’ll have fewer logins and fewer places to mis-click an A record.

Provision the target VPS (Ubuntu 24.04) and lock down basics

For most steady-traffic WordPress sites, start with at least 2 vCPU, 2–4 GB RAM, and NVMe storage. WooCommerce and membership sites usually need more headroom, especially during spikes.

You can do this on a HostMyCode VPS and resize later without rebuilding everything.

Update packages and create a non-root admin

sudo apt update && sudo apt -y upgrade
sudo adduser deploy
sudo usermod -aG sudo deploy

Set timezone and basic tools

sudo timedatectl set-timezone UTC
sudo apt -y install curl unzip rsync git ufw htop

Firewall: allow only what you need

Keep the rules tight: SSH, HTTP, HTTPS.

Hosting mail or DNS on the same VPS is possible. It also adds ports and failure modes most WordPress sites don’t need.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

If you want a stronger baseline (without accidentally cutting off SSH), follow our UFW firewall setup tutorial after the migration is stable.

Install a WordPress-ready web stack (Nginx + PHP-FPM)

This walkthrough uses Nginx because it’s predictable on smaller VPS plans. It’s also efficient with static assets.

If you’re currently on Apache or LiteSpeed, you can still migrate using this plan. Just match PHP first, then validate rewrite/permalink behavior.

Install Nginx and PHP

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

Install MariaDB (or use separate DB hosting)

For many sites, MariaDB on the same VPS is fine. If you want tighter isolation, move the database later as a separate project.

Don’t stack changes during cutover unless you have to.

sudo apt -y install mariadb-server
sudo mysql_secure_installation

Create database and user

sudo mysql -u root -p
CREATE DATABASE wpdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'REPLACE_WITH_STRONG_PASSWORD';
GRANT ALL PRIVILEGES ON wpdb.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Create Nginx server block

Create /etc/nginx/sites-available/example.com and adjust the domain and PHP socket version to match your install.

sudo nano /etc/nginx/sites-available/example.com
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;

  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 ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff2?)$ {
    expires 7d;
    add_header Cache-Control "public, max-age=604800";
    try_files $uri =404;
  }
}
sudo mkdir -p /var/www/example.com/public
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo nginx -t && sudo systemctl reload nginx

Copy WordPress files with rsync (fast, repeatable)

Rsync is ideal here because you can run it as many times as you want. The first run moves everything.

The last run catches only the changed files. That keeps your final window short.

Find your source docroot

Typical locations include:

  • cPanel: /home/USERNAME/public_html
  • Generic Nginx/Apache: /var/www/example.com/public, /var/www/html

Initial file sync (source → target)

From the target VPS, pull from the source host. This avoids opening inbound access on the new server.

sudo rsync -avz --progress \
  -e "ssh -p 22" \
  user@SOURCE_IP:/path/to/wordpress/ \
  /var/www/example.com/public/

If your source host is shared hosting without SSH, you may be stuck doing a one-time SFTP/FTP export.

For repeatable migrations, it’s often worth using a migration service.

HostMyCode can help via migration support when the source environment is restricted.

Fix ownership and permissions

On Ubuntu, Nginx typically runs as www-data.

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 {} \;

Export and import the database safely

Database moves fail for predictable reasons: the wrong character set, missing privileges, timeouts, or a dump taken mid-write.

Aim for a consistent export and a clean import. After import, verify WordPress can read and write.

On the source: create a dump

If you have shell access, mysqldump with these options is a solid baseline for WordPress.

mysqldump -u DBUSER -p \
  --single-transaction --quick --routines --triggers \
  --default-character-set=utf8mb4 \
  DBNAME > /tmp/wp.sql

Then copy it to the target:

scp user@SOURCE_IP:/tmp/wp.sql /tmp/wp.sql

On the target: import

sudo mysql -u wpuser -p wpdb < /tmp/wp.sql

Update wp-config.php database values

Edit /var/www/example.com/public/wp-config.php:

define('DB_NAME', 'wpdb');
define('DB_USER', 'wpuser');
define('DB_PASSWORD', 'REPLACE_WITH_STRONG_PASSWORD');
define('DB_HOST', 'localhost');

Handle the URL change the safe way (no broken logins)

If the domain stays the same, you usually don’t need to replace URLs in the database.

If the domain changes, or you’re testing on a staging host, control the URLs on purpose. This prevents redirect loops, cookie issues, and admin sessions that won’t stick.

Option A (recommended for pre-cutover testing): hosts file override

This lets you load example.com from the new VPS without changing public DNS.

  • On macOS/Linux: edit /etc/hosts
  • On Windows: edit C:\Windows\System32\drivers\etc\hosts

Add:

NEW_VPS_IP example.com
NEW_VPS_IP www.example.com

Your machine will hit the new VPS, while everyone else still lands on the old server.

Option B: set WordPress home/siteurl temporarily

If you need it for staging validation, add these lines to wp-config.php. Remove them after cutover.

define('WP_HOME', 'https://example.com');
define('WP_SITEURL', 'https://example.com');

Install SSL and verify HTTPS behavior before DNS cutover

Don’t point DNS at a server that only serves HTTP or has a broken chain. Fix TLS first, then cut over.

If you’re using Nginx on Ubuntu, follow our Let’s Encrypt setup guide. If renewals are already failing, use the TLS renewal troubleshooting tutorial to straighten it out before you move traffic.

Minimal Certbot steps (Nginx)

sudo apt -y install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

After installation:

sudo nginx -t && sudo systemctl reload nginx
sudo certbot renew --dry-run

Do a pre-cutover functional test (the boring checks that prevent tickets)

With your hosts-file override active, walk through this list before you touch DNS. It’s faster than debugging after users arrive.

  • Homepage + a few posts: images load, no mixed content warnings.
  • wp-admin: login works, you can save a draft post.
  • Permalinks: visit a deep URL and confirm you don’t get 404s.
  • Uploads: upload an image in Media Library.
  • Contact forms: submit a test and confirm delivery (or logging).
  • Cache/plugin behavior: if you use page cache, clear it and re-test.

Quick diagnostics if you hit a blank page or 500 error

sudo tail -n 100 /var/log/nginx/example.com.error.log
sudo journalctl -u php8.3-fpm --no-pager -n 100

Most failures here are simple. The PHP-FPM socket path doesn’t match, a needed extension is missing, or permissions are wrong.

Fix it now, before DNS changes make it urgent.

Final sync window: freeze changes, resync, and cut over

Near-zero downtime mostly comes down to controlling writes. WordPress is never truly “read-only” in the background.

Carts, comments, sessions, and plugin logs constantly touch the database. Plan for a short freeze and keep it tight.

Step 1: enable a short maintenance window

Pick the least disruptive option for your site:

  • Put the site in maintenance mode with a plugin.
  • At minimum, disable checkout/comments for 5–10 minutes.

Step 2: run a final rsync

sudo rsync -avz --delete \
  -e "ssh -p 22" \
  user@SOURCE_IP:/path/to/wordpress/ \
  /var/www/example.com/public/

Step 3: create a fresh DB dump and import again

Repeat the dump/import so the target has the latest content.

If the database is large, compressing it speeds transfers and reduces hiccups:

mysqldump -u DBUSER -p --single-transaction --quick --default-character-set=utf8mb4 DBNAME | gzip > /tmp/wp.sql.gz
scp user@SOURCE_IP:/tmp/wp.sql.gz /tmp/wp.sql.gz
gzip -dc /tmp/wp.sql.gz | sudo mysql -u wpuser -p wpdb

Step 4: cut DNS (A/AAAA) to the new VPS IP

Update these records:

  • A record: example.comNEW_VPS_IP
  • A record: wwwNEW_VPS_IP (or CNAME to root)

Leave email DNS alone unless email is also moving (MX, SPF, DKIM, DMARC). If MX points elsewhere, changing A records won’t move mail.

Step 5: verify propagation and origin correctness

From your laptop:

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

From a few external resolvers:

dig @1.1.1.1 +short example.com
dig @8.8.8.8 +short example.com

Post-cutover hardening and performance steps (quick wins)

Once traffic lands on the new VPS, do the small fixes that prevent CPU spikes and avoidable support tickets.

1) Confirm real client IPs (if behind a proxy/CDN)

If you use Cloudflare or another proxy, configure Nginx real IP handling. This keeps logs, rate limits, and security rules accurate.

2) Add security headers carefully

Headers help, but an aggressive CSP can break admin pages, embeds, or payment widgets.

Add headers in stages and test as you go. Our Nginx security headers tutorial covers safe defaults and a practical testing flow.

3) Set up a backup you can restore (not just store)

Minimum baseline: daily DB dump, nightly file sync to offsite storage, and a weekly restore test.

If you want an rsync-based setup, follow the rsync offsite backup tutorial.

4) Watch disk and logs for the first 48 hours

Migrations can trigger noisy bots, misconfigured plugins, or redirect loops that inflate logs fast.

If disk space starts dropping, use the playbook in our server storage cleanup tutorial.

Rollback plan (keep it boring, keep it fast)

Your rollback is DNS. That’s the whole point of lowering TTL in advance.

  1. Put the site into maintenance mode on the new VPS.
  2. Switch A/AAAA back to the old server IP.
  3. Verify traffic returns to the old host.
  4. Investigate on the new VPS without pressure.

Keep the old server online for at least 48–72 hours. If revenue is on the line, keep it for a week.

If you want this migration pattern without babysitting the stack, run WordPress on a managed VPS hosting plan from HostMyCode. If you prefer full control, a standard HostMyCode VPS gives you root access and predictable performance for Nginx + PHP-FPM.

FAQ: WordPress migration tutorial questions that come up in real moves

Do I need to migrate email when I migrate WordPress?

Not usually. If your MX records point to Google Workspace, Microsoft 365, or another mail provider, changing your website A record won’t move mail. Only change MX/TXT records if you’re explicitly migrating email too.

How do I avoid losing WooCommerce orders during cutover?

Use a short maintenance window during the final database sync. Disable checkout for 5–10 minutes, do the final DB import, then cut DNS. For very high-order stores, consider a scheduled freeze during low traffic.

Why does the new server show the old site even after DNS changes?

It’s almost always caching: local DNS cache, ISP resolver caching, or a CDN still pointing to the old origin. Check with dig @1.1.1.1 and purge your CDN cache if you use one.

What’s the fastest way to spot a PHP mismatch after migration?

Check /var/log/nginx/*.error.log and journalctl -u phpX.Y-fpm. Missing extensions and fatal errors show up immediately. Match the source PHP version first, then upgrade after stabilization.

Should I move to shared hosting or a VPS for WordPress in 2026?

If you need custom Nginx rules, server-side caching, or consistent resources under load, a VPS is the safer choice. Shared hosting works for lighter sites, but migrations to VPS tend to reduce “random” slowdowns when traffic spikes.

Summary: a repeatable WordPress move you can trust

This workflow keeps the risk contained. Stage the new VPS, sync files and database, validate through a hosts override, then cut DNS with a short TTL.

Downtime stays minimal, and rollback is a simple DNS change.

If you want help moving from shared hosting to a VPS (or between VPS providers), HostMyCode can handle the heavy lifting through migration services. You can run the final setup on a HostMyCode VPS sized for your traffic.

WordPress Migration Tutorial (2026): Move a Site to a New VPS with Near-Zero Downtime | HostMyCode