📑 Daftar Isi
- Phase 2: Lower DNS TTL — At Least a Week Before
- Phase 3: Provision and Harden the New Server
- Phase 4: Copy Files with Rsync
- Phase 5: Keep the Database in Sync
- Phase 6: The Cutover
- Phase 7: Post-Migration Verification
- Rollback: Because Plans Fail
- Common Migration Problems and Quick Fixes
- FAQ: Migrating a VPS Between Providers
Let’s skip the small talk. You’re here because you need to move a VPS to a different provider, and “okay, we’ll do a 30-minute maintenance window” isn’t going to fly with your users. So here’s the plan: spin up the new server, keep it in sync with the old one, flip the DNS, and only kill the old box when you’re absolutely sure. Zero downtime isn’t a magic trick — it’s a sequence of well-orchestrated steps. I’ve done this dozens of times, and this is exactly the playbook I use.
Before we dive in, one honest warning: if you’re on a shared host with no root access, stop reading. This guide assumes full root on both servers, because migrating a VPS between providers without downtime requires control over DNS, cron jobs, and system services on both sides. If you’re on cPanel, most of the file-transfer logic still applies, but you’ll want a proper cPanel migration approach instead.
The core idea is simple. You don’t move a running server by stopping it. You stand up a twin, keep the data synchronized for a while, then switch traffic over. Think of it like replacing the wheels on a car while it’s still rolling — you don’t jack up the whole car and stop everything; you do it one corner at a time. The tricky parts are the same three things that break every migration: changing data (databases), stale DNS caches, and missed configuration. This guide handles all three, in order, with commands you can copy.
Quick checklist of what you’ll need before we start: SSH access to both servers, control over your DNS records, at least a week of lead time (non-negotiable, I’ll explain why in a minute), and a way to test the new server before the switch. Got all that? Good. Let’s break it into phases.

First, you need to know exactly what you’re migrating. Skipping this is how servers get lost. Run these on the old box and write down the output:
cat /etc/os-release
nproc
free -h
df -h
List running services and what’s enabled at boot:
systemctl list-units --type=service --state=running
systemctl list-unit-files --state=enabled
Grab the cron jobs — trust me, everyone forgets these and then wonders why backups stopped:
crontab -l
ls /etc/cron.d/ /etc/cron.daily/
And see what’s listening on the network:
ss -tulpn
While you’re at it, check for integrations that talk TO this server. Payment webhooks, monitoring agents, external cron services that hit an IP directly — anything with a whitelisted IP or a hardcoded endpoint. Those break silently on migration, and you won’t notice until the client’s invoice fails. Document them all in a file that lives OUTSIDE the server. A Google Doc, a notepad, anything. If the old server dies mid-migration, your notes shouldn’t die with it.
Phase 2: Lower DNS TTL — At Least a Week Before
This is the step everyone wants to skip, and it’s the one that ruins migrations. Check your current TTL first:
dig +short example.com SOA
dig example.com A
If you see TTL values like 86400, that’s 24 hours. It means after you change the A record, resolvers around the world can keep pointing users at the old IP for a full day. Change every relevant record (A, AAAA, MX, CNAME) to a TTL of 300 seconds. Do it now, not on migration day.
Why a week? Because changing the TTL doesn’t instantly propagate. Resolvers cache the OLD TTL value too. Set a 5-minute TTL today, and some resolvers will still hold the previous 24-hour value for up to a day. Give yourself a 7-day buffer so the entire DNS ecosystem has switched to the low TTL before you cut over. This single step is the difference between a clean switch and a two-day headache.
Phase 3: Provision and Harden the New Server
Order the new VPS from the target provider with the same OS version as the old one. Same version, not newer. You’re migrating, not upgrading — keep the variables to a minimum. Match the application stack too. PHP 8.3 won’t cleanly take over for PHP 7.4 configs, so check versions on the old box:
php -v
mysql --version
nginx -v
Install the matching versions on the new server. Do basic hardening before any data arrives:
apt update && apt upgrade -y
apt install -y fail2ban
systemctl enable --now fail2ban
Set up key-based SSH and test it:
ssh-keygen -t ed25519
ssh-copy-id root@203.0.113.10
Disable password auth once you’ve confirmed the key works, and consider changing the SSH port. Locked-down is easier to do now than on a server with real traffic on it. For a deeper look at this, check out our guide on hardening SSH on a fresh VPS.
Phase 4: Copy Files with Rsync
Rsync is your workhorse here because it’s incremental — run it once for a baseline, then again right before cutover and it only transfers the changes. Baseline copy of your application data:
rsync -avz --delete -e ssh root@OLD_IP:/home/ /home/
If your web roots live elsewhere, adjust the paths:
rsync -avz --delete -e ssh root@OLD_IP:/var/www/ /var/www/
Two rules I always follow. First, always test with –dry-run first. Source then destination, in that order, and dry-run saves you from the classic reversed-rsync disaster:
rsync -avz --dry-run --stats -e ssh root@OLD_IP:/var/www/ /var/www/
Second, don’t blindly copy server-specific configs like /etc/nginx/nginx.conf or /etc/mysql/my.cnf. Copy them, but diff and adjust manually — they contain IPs, paths, and hostnames that differ on the new box. And exclude logs and backups from the rsync unless you actually want to haul them over:
rsync -avz --delete --exclude 'logs' --exclude 'backup'
-e ssh root@OLD_IP:/var/www/ /var/www/
Schedule it like this: one baseline sync early on (slow, moves everything), then a final quick sync right before cutover (fast, moves only the delta). If you want to automate this properly later, we have a write-up on automating VPS backups with rsync that builds on this.
Phase 5: Keep the Database in Sync
Files are easy. Databases are where migrations go to die, because data keeps changing while you copy. If your app writes constantly, a single dump is data loss waiting to happen. Two options depending on your situation.
Option A — for small databases where you can afford a brief sync window. Dump with consistency in mind:
mysqldump -u root -p --all-databases
--single-transaction --routines --triggers --events
> alldb.sql
Then import:
mysql -u root -p < alldb.sql
Remember: –single-transaction only protects InnoDB. If you have MyISAM tables, the dump will lock them, so run it during a quiet window.
Option B — real replication, the zero-downtime standard. On the old server (master), edit your MySQL config:
[mysqld]
server-id = 1
log_bin = /var/log/mysql/mysql-bin
binlog_format = ROW
Restart MySQL, then create a replication user:
CREATE USER 'repl'@'203.0.113.10' IDENTIFIED BY 'strong-unique-password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'203.0.113.10';
FLUSH PRIVILEGES;
Note the binlog coordinates:
SHOW MASTER STATUS;
Expected output:
+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000042 | 1234567 | | |
+------------------+----------+--------------+------------------+
Dump and import a baseline to the new server, then point it at the master:
CHANGE MASTER TO
MASTER_HOST='203.0.113.10',
MASTER_USER='repl',
MASTER_PASSWORD='strong-unique-password',
MASTER_LOG_FILE='mysql-bin.000042',
MASTER_LOG_POS=1234567;
START SLAVE;
Always verify:
SHOW SLAVE STATUSG
You want these two lines to say Yes:
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
If the IO thread says No, it’s usually a firewall or credentials problem. Test the connection directly:
nc -zv 203.0.113.10 3306
And remember, firewalls are two-way. The new server must be able to reach the old one’s 3306, and vice versa. Also match timezone and character set between both servers before replication starts, or you’ll get weird data on the slave. For a full deep-dive on this topic, our MySQL/MariaDB realtime replication guide covers it end to end.
Phase 6: The Cutover
Everything’s in sync. Now test the new server in isolation before touching DNS. Add an entry to your local hosts file so you can reach the app at the new IP:
echo "203.0.113.10 example.com" >> /etc/hosts
Walk through the app. Log in, check the features your users actually touch, verify static assets load. Fix anything broken now, not after cutover.
When it looks good, update the DNS records to the new IP. Because you lowered the TTL a week ago, traffic shifts over in minutes, not hours. Watch it migrate:
tail -f /var/log/nginx/access.log
Keep the old server running for the next 24-48 hours. Do not shut it down. You’ll see the access log taper off as resolvers switch. If you need to roll back, it’s just a DNS change away. Patience here is what separates professionals from the people who get paged at 3am.
Phase 7: Post-Migration Verification
Migration isn’t done when traffic moves. It’s done when everything is verified. Run through this list:
- Every domain returns HTTP 200, no 502s or 504s.
- SSL certs are valid and serve the right hostnames.
- Email flows (if MX moved): send tests to Gmail, Yahoo, Outlook.
- Cron jobs fire: check today’s scheduled jobs actually ran on the new box.
- External integrations work: update IP whitelists for payment gateways and webhooks.
- Disk, CPU, and RAM look healthy under real traffic.
Set up a simple polling loop for a few hours:
while true; do
curl -o /dev/null -s -w "%{http_code} %{time_total}sn" https://example.com
sleep 60
done
Green across the board? You’re practically done.
Rollback: Because Plans Fail
Here’s the beauty of doing it this way: rollback is a DNS record change. Old server still running, old data still intact, so point DNS back at the old IP and traffic returns within minutes. You’ll lose whatever changed during the window, but you won’t be down, and that’s the whole point.
Don’t disable replication or stop cron on the old server until you’ve verified the new one for days. Only then shut services down, one by one. And take one final backup of the old server before you retire it — a snapshot or a full rsync to a safe place. It’s cheap insurance, and it has saved me more than once.
Common Migration Problems and Quick Fixes
| Symptom | Likely Cause | Quick Fix |
|---|---|---|
| Users still hitting old server after cutover | Stale DNS TTL on some resolvers | Wait out the TTL; verify via dns.google from multiple locations |
| Slave_IO_Running: No | Port 3306 blocked or bad credentials | nc -zv check, review firewall, re-run CHANGE MASTER with fresh coordinates |
| 502 Bad Gateway on new server | PHP-FPM or app service not started; version mismatch | systemctl status php*-fpm, match versions with old server |
| Disk fills up after rsync | Logs or backups dragged across | Exclude logs/backup paths, then du -sh / to find the hog |
| Email rejected after migration | MX not moved, or PTR/rDNS missing on new IP | Set reverse DNS in provider panel, verify dig +short MX domain |
| Slow SSH on the new box | Reverse DNS lookup enabled in sshd | Set UseDNS no, restart sshd |
Anything not in the table? Read the logs. Logs tell the whole story — start at the beginning, not at the word “error”. The root cause is almost always a few lines before the panic message. Read the whole block, trace the sequence, then act.
FAQ: Migrating a VPS Between Providers
Q: Can I keep the same IP when moving to a different provider?
Only if you own the IP block and the new provider supports bring-your-own-IP. In most cases the IP changes, so you manage the switch through DNS. That’s exactly why lowering the TTL a week in advance matters so much.
Q: What’s the actual downtime with this approach?
Near zero during normal operation. The only real downtime is a few seconds around the cutover for sessions that were mid-request, and even that disappears if your application is stateless or handles session migration gracefully. The old server stays up the whole time as a safety net.
Q: Should I use mysqldump or replication for the database?
For anything that writes data continuously, use replication. A dump is a snapshot; by the time you restore it, the source has moved on, and you’ve lost data. Replication keeps the slave current to within seconds, making cutover clean. Use dumps only for small, mostly-read databases.
Q: How long before cutover should I lower the DNS TTL?
At least a week. The old TTL value is cached by resolvers, so the full switch to a 300-second TTL takes up to the length of your previous TTL to propagate. A 7-day buffer is the safe standard.
Q: When is it safe to shut down the old server?
After you’ve verified the new server under real traffic for at least 24-48 hours and the old server’s access logs show no more hits. Shut it down gradually — stop services one by one — and take a final backup before retiring it.
That’s the whole playbook. Inventory, low TTL, twin server, rsync, replication, cutover, verify, rollback on standby. Follow it in order and you’ll move between providers without your users ever noticing. Most of the drama people have with migration comes from skipping one of these phases — usually the TTL one.
If you want to go deeper on the pieces, here are a few related reads from our archive: how to automate VPS backups with rsync, a deep dive into MySQL/MariaDB replication, and troubleshooting DNS propagation when cutover gets weird. Also worth a look: hardening SSH on a fresh VPS before you move anything onto it. Got a better trick? Drop it in the comments — always keen to hear how other teams do it.