Server Migrations with Minimal Downtime
My goal for a migration is for the cutover to be boring. Files showing up on the new server isn't success. Success is when the data, the behavior, the identity (IPs, hostnames, certificates), and the operational stuff (backups, monitoring) have all moved and nobody noticed. The migrations that go well are done in stages, measured, and can be undone.
Find everything before copying anything
The website people see is often a small part of what's actually running. Before I copy a byte, I want a list of:
- Domains, subdomains, parked domains, redirects, and DNS zones
- Website files, symlinks, permissions, ownership, and any special attributes
- Databases, users, grants, stored routines, events, and character sets
- PHP (or other runtime) versions, extensions, modules, and custom settings
- Mailboxes, forwarders, filters, autoresponders, aliases, and the mail routing setting
- Cron jobs, queues, scheduled tasks, and background workers
- Certificates, private keys, how ACME renewals work, and any CDN or proxy in front
- Outside services that allowlist the server's IP: payment callbacks, webhooks, SFTP partners, monitoring
- Backup jobs and how restores work
- Customer contacts, maintenance restrictions, and who has to sign off
Look for hidden assumptions
Apps love to hard-code IP addresses, file paths, hostnames, and assumptions about how mail gets sent. Go find them before cutover, not after:
# Things worth finding before a move
grep -R "192\.0\.2\." /home/example/public_html /etc 2>/dev/null
grep -R "/home/olduser" /home/example 2>/dev/null
grep -R "old-hostname" /home/example /etc/cron* 2>/dev/null
# Timers and cron jobs
systemctl list-timers --all
crontab -l
Prove compatibility early
A lot of failed migrations are really compatibility problems that got discovered too late. Compare old and new well before the maintenance window:
| Area | Questions |
|---|---|
| OS | Are paths, service names, packages, SELinux/security policies, or filesystem behavior different? |
| Web server | Does the app depend on .htaccess overrides, NGINX rules, LiteSpeed quirks, or custom modules? |
| PHP / runtime | Are the right versions and extensions there? Any deprecated functions or stricter defaults? |
| Database | SQL modes, auth plugins, collations, and engine versions? |
| Will local vs. remote routing, DKIM, SPF, PTR, or outbound reputation change? | |
| Network | Are firewall rules, IP allowlists, NAT, IPv6, and MTU still valid? |
Test with the hard ones
Don't just test the easiest account. Pick a few that cover:
- The biggest account
- The oldest app
- A domain that does a lot of mail
- Something with scheduled tasks
- A site that calls outside APIs or is on someone's IP allowlist
- A customer with a dedicated IP or custom DNS
Picking a method
Cold migration
Stop everything, copy it all once, test, start it on the new server. Simple and consistent, but the longest outage.
Pre-copy and final sync
Copy most of the data while the old server is still live, then stop writes for a short final sync. This is my usual choice.
Replication
Continuously ship database or storage changes, then promote the new side. Less downtime, more setup and more ways for it to get inconsistent.
Proxy bridge
Have the old server forward requests that still reach it to the new one while DNS catches up. Useful, but don't let it cover up unfinished migration work.
The pre-copy pattern
# First pass while the old server is still live
rsync -aHAXx --numeric-ids --info=progress2 \
/home/example/ root@new-server:/home/example/
# During the window, stop writes, then sync what changed
rsync -aHAXx --numeric-ids --delete --info=progress2 \
/home/example/ root@new-server:/home/example/
Be very careful with --delete. A missing trailing slash or swapped source and destination can wipe out data. Double-check the paths, and try the exact command on non-production data first. --dry-run is your friend.
The runbook
- Plan. Scope, who owns what, the window, customer notices, what success looks like, and the rollback deadline.
- Get the new server ready. Patched, networked, runtimes installed, backups running, monitoring enrolled.
- Lower DNS TTLs. Early enough that the old TTL has fully expired before the cutover.
- Pre-copy. Move the big, mostly static stuff ahead of time.
- Test privately. Hosts file override, temporary hostname, isolated VLAN, or a test listener.
- Freeze writes. Maintenance mode, stop jobs, whatever it takes to get a consistent final state.
- Final sync. Changed files, databases, mail, and config.
- Cut over. DNS, routing, NAT, proxy, whatever points traffic at the new server.
- Test for real. From several networks, using real workflows.
- Watch. Keep the old server around (but locked down) until the rollback window closes.
- Wrap up. Update inventory, backups, monitoring, docs, and let customers know it's done.
Mail and DNS need their own plan
During a DNS change, mail can land on both the old and new servers. Plan for it instead of assuming every sender picks up the new MX record right away.
Ways to handle mail
- Copy mailboxes ahead of time, then do a final sync after cutover.
- Temporarily forward or proxy mail from the old server to the new one, if you can.
- Leave the old server accepting mail and do a second sync after DNS settles.
- If mail is hosted elsewhere (Microsoft 365, Google), make sure the migration doesn't flip the account to local delivery.
DNS checklist
- You know which nameservers are authoritative.
- A, AAAA, MX, CNAME, TXT, SRV, CAA, and anything custom are all listed.
- You understand the DNSSEC setup and DS records.
- SPF and DKIM will still be valid with the new IP.
- PTR records are lined up for new mail IPs.
- TTL changes have had time to take effect.
- Monitoring checks the authoritative servers, not just a cached answer.
Test the service, not just the port
Being able to connect to port 443 proves almost nothing. Test whole workflows:
| Layer | What I check |
|---|---|
| DNS | Authoritative and cached answers, IPv4 and IPv6, DNSSEC, MX, PTR |
| TLS | Right certificate, full chain, hostname, SNI, expiration, and the proxy path |
| Web | Homepage, login, uploads, downloads, redirects, static files, error pages |
| App | Create/update/delete, background jobs, webhooks, external APIs |
| Database | Row counts, encoding, routines, events, writes working, backups running |
| Inbound, outbound, local and remote delivery, auth, webmail | |
| Operations | Monitoring, logs, backups, alerts, and support can get in |
Test from outside
Test from outside the new server's network. Internal DNS, hairpin NAT, trusted IPs, and local certificate stores can all make a broken public service look fine from the inside.
Rollback is a decision, not a hope
Decide your rollback point before you start. Once both servers have taken writes independently, just flipping DNS back can lose data or create a mess.
When I'd roll back
- An important workflow is broken and can't be fixed within the window.
- You can't prove the data is consistent.
- Performance is clearly worse than it needs to be.
- Mail routing or reputation makes delivery too risky.
- Monitoring or backups aren't set up yet.
- You've lost safe admin access.
Don't delete the old server right away
Lock it down, turn off scheduled jobs that don't need to run, label it clearly, and keep it for the rollback or verification period you agreed on. When you finally decommission it:
- Take a final archive, per your retention policy.
- Remove it from DNS, monitoring, backups, and automation.
- Release licenses and IPs.
- Wipe the disks properly.
- Update diagrams and inventory.
cPanel notes
WHM's Transfer Tool can copy accounts and some config between cPanel servers, and the live transfer feature can cut down on visible downtime. You still need to read the transfer warnings, test accounts, and leave enough space on the destination for the archives and unpacking.
