Back to tutorials
Tutorial

Server migration tutorial (2026): Move a Website to a New VPS with rsync, DNS cutover, and rollback

Server migration tutorial for 2026: move a site to a new VPS with rsync, DNS cutover, SSL checks, and a clean rollback plan.

By Anurag Singh
Updated on Aug 13, 2026
Category: Tutorial
Share article
Server migration tutorial (2026): Move a Website to a New VPS with rsync, DNS cutover, and rollback

The difference between a calm cutover and a 2 a.m. incident usually comes down to one thing: rehearsal. This server migration tutorial walks through a practical, repeatable way to move a Linux-hosted website to a new VPS. You’ll use rsync, a controlled DNS switch, and a rollback you can execute quickly.

These steps assume Ubuntu 24.04 LTS on both servers, Nginx or Apache, and a PHP site (WordPress-friendly). You can follow the same flow on AlmaLinux/Rocky. Expect different paths and service names.

What you’ll migrate (and what you should not)

Most website moves come down to four buckets: site files, web server config, TLS/SSL, and the small-but-critical runtime pieces (cron, mail settings). A common mistake is cloning the whole server and hoping the new box “sorts it out.” Don’t do that.

  • Do migrate: /var/www (or your document roots), vhost configs, TLS certificates (or re-issue), cron, firewall rules, and server-level limits (PHP-FPM pools, upload sizes).
  • Be careful with: /etc, whole-disk rsync, and blindly restoring user accounts. You’ll drag old mistakes with you.
  • Usually don’t migrate: OS-level caches, logs, and temporary directories.

Prerequisites checklist before you touch DNS

Run this list on the old server and the new server. It helps prevent the classic “SSH looks fine, browser is broken” surprise.

  • New VPS is provisioned, updated, and reachable over SSH with keys.
  • You know your domain’s current DNS provider and TTL values.
  • You can access the old server as root (or via sudo).
  • You have enough disk on the new server for files plus at least 30% headroom.
  • You have a rollback window: keep the old server live for 48–72 hours.

If you’re moving production sites for clients or juggling multiple domains, starting on a managed VPS hosting plan can save time. OS updates, baseline hardening, and basic safety checks won’t compete with your migration window.

Step 1 — Prepare the new VPS (users, packages, and time)

Make the new server predictable before you copy anything. Set time correctly, install your tools, and keep the filesystem layout clean.

sudo apt update && sudo apt -y upgrade
sudo apt -y install rsync curl unzip ca-certificates
sudo timedatectl set-timezone UTC
timedatectl status

If you’re using Nginx + PHP-FPM:

sudo apt -y install nginx php-fpm

If you’re using Apache:

sudo apt -y install apache2

Confirm the service starts cleanly:

systemctl status nginx --no-pager
# or
systemctl status apache2 --no-pager

Step 2 — Lower DNS TTL ahead of the cutover

Lowering TTL doesn’t “push” DNS updates faster. It reduces how long resolvers keep the old IP. Change TTL at least 12–24 hours before you plan to switch.

  • Set the A / AAAA record TTL to 300 seconds (5 minutes) if your DNS provider allows it.
  • Do the same for www, API subdomains, and any other hostnames you’ll move.

If you’re also changing nameservers or DNS providers, follow our detailed DNS migration tutorial first. Nameserver changes fail differently than a simple A record update.

Step 3 — Create a migration user (optional but cleaner)

You can rsync as root. A dedicated user is safer and reduces “oops” moments. On the old server:

sudo adduser --disabled-password --gecos "" migrator
sudo usermod -aG sudo migrator

Copy your SSH key to that account (from your workstation):

ssh-copy-id migrator@OLD_SERVER_IP

Then test sudo access:

ssh migrator@OLD_SERVER_IP "id && sudo -n true && echo ok"

Step 4 — Inventory what’s actually running

Before you copy files, capture what the old server is doing. Think of this as a build sheet for the new VPS.

Find vhosts and document roots

# Nginx
sudo nginx -T 2>/dev/null | grep -E "server_name|root\s" | head -n 80

# Apache
sudo apachectl -S

List cron jobs

sudo crontab -l
sudo ls -la /etc/cron.* /etc/crontab

Capture enabled services

systemctl list-unit-files --type=service --state=enabled | sed -n '1,200p'

Step 5 — Copy site files with rsync (first sync)

This is where migrations often go sideways. scp is simple, but it’s easy to lose owners, permissions, symlinks, and edge cases. rsync is designed for this.

On the new VPS, create the target directory:

sudo mkdir -p /var/www
sudo chown -R root:root /var/www

Run a first-pass sync from old to new. Pulling from the new server is common and usually easier on firewalls:

sudo rsync -aHAX --numeric-ids --info=progress2 \
  --exclude='*/cache/*' \
  --exclude='*/wp-content/cache/*' \
  --exclude='*/wp-content/uploads/cache/*' \
  migrator@OLD_SERVER_IP:/var/www/ /var/www/

Notes: -aHAX preserves permissions, hardlinks, ACLs, and xattrs where possible. If the source filesystem doesn’t use ACLs/xattrs, the flags won’t cause problems.

Step 6 — Move your web server configs (and don’t overwrite defaults)

Copy only what you need. Virtual hosts and snippets are usually enough. Avoid copying all of /etc.

Nginx (common layout):

sudo rsync -a --info=progress2 \
  migrator@OLD_SERVER_IP:/etc/nginx/sites-available/ /etc/nginx/sites-available/

sudo rsync -a --info=progress2 \
  migrator@OLD_SERVER_IP:/etc/nginx/snippets/ /etc/nginx/snippets/

Recreate symlinks for enabled sites. Do it deliberately, one site at a time:

ls -1 /etc/nginx/sites-available
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com

Apache:

sudo rsync -a --info=progress2 \
  migrator@OLD_SERVER_IP:/etc/apache2/sites-available/ /etc/apache2/sites-available/

Then enable only the sites you intend to serve:

sudo a2ensite example.com.conf
sudo systemctl reload apache2

If the old setup used a reverse proxy, keep client IP handling intact. For Nginx behind Cloudflare or a load balancer, follow Nginx real IP configuration. Otherwise logs, rate limits, and geo rules may see your proxy instead of real visitors.

Step 7 — Handle SSL/TLS the right way (re-issue or migrate)

You have two reasonable options:

  • Preferred: re-issue Let’s Encrypt certificates on the new server once the domain can reach it (or using a temporary validation method).
  • Alternative: copy existing certificates and keys if you understand the risks and keep permissions tight.

If you’re using Let’s Encrypt with Certbot, re-issuing is usually the cleanest move. Use our Let’s Encrypt setup guide if you want a known-good Certbot baseline on Ubuntu.

Temporary hosts-file test (no DNS change yet)

From your workstation, point the domain at the new IP. This lets you test the site end-to-end before cutover:

# Linux/macOS: edit /etc/hosts
NEW_SERVER_IP   example.com www.example.com

Now browse normally. You’ll hit the new VPS while everyone else still sees the old server. Remove the hosts entry after testing.

Step 8 — Freeze writes briefly, then run the final rsync

Even “mostly static” sites change. WordPress writes uploads, caches, and background job output. Plan a short maintenance window so you don’t miss writes between syncs.

For WordPress: enable a maintenance mode plugin or create a simple .maintenance flow.

If you can, pause cron runners and avoid admin changes during the window.

Run a final sync right before you switch DNS:

sudo rsync -aHAX --numeric-ids --delete --info=progress2 \
  --exclude='*/cache/*' \
  --exclude='*/wp-content/cache/*' \
  migrator@OLD_SERVER_IP:/var/www/ /var/www/

--delete removes destination files that no longer exist on the source. It keeps the new server tidy. Only use it if you trust the source tree.

Step 9 — Smoke-test on the new server (fast, boring checks)

Don’t guess. Run a few checks that fail loudly and point at the right log.

  • Config test:
sudo nginx -t && sudo systemctl reload nginx
# or
sudo apachectl configtest && sudo systemctl reload apache2
  • HTTP status:
curl -I http://127.0.0.1/
curl -I http://127.0.0.1/ | head
  • Tail logs while loading a page:
sudo tail -f /var/log/nginx/error.log
# or
sudo tail -f /var/log/apache2/error.log

If performance is a concern, tune before the cutover. It’s easier to adjust worker counts and PHP-FPM settings without live users hitting errors. Our VPS performance optimization tutorial covers safe defaults for PHP-FPM, worker sizing, and buffering. These changes often help WordPress immediately.

Step 10 — Cut over DNS (A/AAAA record change)

At cutover time, change the A record (and AAAA if you use IPv6) to the new server IP. Because you lowered TTL earlier, most clients should switch within minutes.

Verify from multiple resolvers:

# Query authoritative / public resolvers
dig +short example.com @1.1.1.1
dig +short example.com @8.8.8.8

# Check what your system resolver sees
getent hosts example.com

Then confirm requests are landing on the new server. A simple technique is to add a temporary header on the new VPS and look for it. For Nginx, add this inside the relevant server block:

add_header X-Migrated-To "new-vps" always;

Reload Nginx and check:

curl -I https://example.com | grep -i x-migrated

Remove the header once you’re satisfied.

Step 11 — Post-cutover checks (the stuff users actually notice)

Give yourself 30 minutes to walk real user paths. If something is wrong, you’ll usually find it here.

  • Login flows (WordPress / customer portals)
  • Uploads (media library, forms with attachments)
  • Transactional email (password reset, order confirmation)
  • Payment integrations and webhook callbacks
  • 404/500 rates in logs

If mail is also hosted on the server, check deliverability basics immediately. A wrong PTR record or missing SPF can trigger rejections. Use the existing PTR record setup tutorial to avoid the “site works, email bounces” scenario.

Step 12 — Keep rollback simple (and rehearse it once)

Rollback isn’t defeat. It’s risk control.

You have two common rollback options:

  • DNS rollback: point the A/AAAA record back to the old IP. Fastest for web-only moves.
  • Traffic steering: if you use a CDN/proxy, point origin back to the old server.

Before you call the migration “done,” confirm two things:

  • The old server still serves the site correctly if the A record changes back.
  • You know where writes went during the cutover window (uploads, orders, form submissions).

A practical compromise is to keep the old server read-only after cutover (where possible). Also block admin access. This prevents content from diverging across two machines.

Step 13 — Clean-up after 48–72 hours

Once traffic has settled and your logs are quiet:

  • Raise DNS TTL back to 3600–14400 seconds (1–4 hours) for stability.
  • Remove temporary migration headers and hosts-file entries.
  • Enable automatic updates cadence (security patches) and configure monitoring.

For monitoring, alert on disk, load, and HTTP error rates. If you want a straightforward setup, follow our server monitoring tutorial. Hook alerts up before you decommission the old server.

Troubleshooting: common migration failures (and quick fixes)

  • Site loads but styles/images are broken: mixed content or wrong base URL. Check hardcoded http:// URLs and CDN settings.
  • Random 403/404 after rsync: ownership/permissions changed. For Nginx/PHP apps, confirm the web user can read files: namei -l /var/www/example.com.
  • SSL works for the root domain but not www: missing SAN in certificate or wrong vhost match order. Verify your server_name blocks and re-issue cert with both names.
  • Seeing old content after cutover: caching. Clear CDN cache, browser cache, and confirm resolver results with dig.
  • 502/504 errors: PHP-FPM socket/path mismatch. Check your Nginx upstream. Then run systemctl status php8.3-fpm and tail /var/log/php8.3-fpm.log if present.

Summary: the safe migration pattern you can reuse

This pattern stays intentionally boring because it works. Lower TTL, run a first rsync, validate configs, freeze writes briefly, run a final rsync, switch DNS, and keep rollback ready.

The same sequence scales from one WordPress site to a small fleet.

If you’d rather start from a clean, predictable server, deploy the destination on a HostMyCode VPS sized for your real traffic. Extra CPU and NVMe headroom helps during the cutover rush. It also pays off after you’ve moved.

If you’re migrating a production site and want fewer moving parts, run the destination on managed VPS hosting from HostMyCode. You still control your stack, but you have backup when patching, firewall basics, or service restarts threaten your timeline. For straightforward projects, a standard HostMyCode VPS is a clean, cost-effective migration target.

FAQ

How long should I keep the old server after the DNS cutover?

Keep it for at least 48–72 hours. That covers most resolver caches and gives you time to spot missed background jobs or rare page paths.

Can I migrate without downtime?

You can get near-zero downtime for many sites by doing a first rsync days ahead, then a brief write freeze for the final sync. For stores and apps with frequent writes, plan the smallest possible maintenance window.

Should I copy Let’s Encrypt certificates or re-issue them?

Re-issue on the new server when possible. It avoids key handling mistakes and ensures your renewal automation is tied to the new machine.

What’s the safest way to test the new server before DNS changes?

Edit your local hosts file to point the domain to the new IP, then browse and run curl checks. It’s quick and doesn’t affect real users.

What if email is on the same server I’m migrating?

Plan email separately: confirm MX, SPF, DKIM, DMARC, and PTR are correct for the new IP. Test deliverability before you switch high-volume sending.