How to Move Server Go New VPS Without Wahala
Move your live server go new VPS with a rehearsed cutover: inventory first, rebuild not clone, dump database, lower DNS TTL early, verify, then switch.
Migrate server go new VPS as cutover wey you don rehearse
To migrate server go new VPS, treat the move like cutover wey you don rehearse, no be simple copy. Build the new box from scratch, sync the data two times, confirm say the new box dey work with im own IP address before you touch DNS, then switch the records and leave the old box running until you sure say everything dey okay. Copying the bytes na the easy part. The order of operations go decide whether the move go smooth or cost plenty.
This guide cover one Linux server wey dey run web application, database, and TLS (transport layer security) certificate. This setup cover most single-server arrangements. Two hosts dey involved, so every example get comment wey show which host e dey run on. The addresses come from documentation ranges: 198.51.100.10 na the old server, 203.0.113.20 na the new one.
Read the whole runbook before you start. The first step, lowering the DNS TTL, must happen days before the step wey you really care about.
Inventory first before you build anything
You no fit rebuild server wey you never describe. Spend one hour write down wetin the old box dey do. The thing wey dey break after migration na always the one wey nobody remember: a cron job, a firewall exception, or an environment file wey dey outside the application directory.
Run these for the old server and keep the output somewhere wey you fit read am from the new one.
# old server: packages you asked for, not the dependencies they pulled in
apt-mark showmanual > ~/inv-packages.txt
# old server: what runs now, and what starts at boot
systemctl list-units --type=service --state=running --no-pager > ~/inv-services.txt
systemctl list-unit-files --state=enabled --no-pager >> ~/inv-services.txt
systemctl list-timers --all --no-pager > ~/inv-timers.txt
# old server: what is listening, on which address and port
sudo ss -tulpn > ~/inv-ports.txt
# old server: human accounts, skipping the system ones
awk -F: '$3 >= 1000 && $3 < 65534 {print $1, $3, $6, $7}' /etc/passwdapt-mark showmanual na the list wey worth keeping because e go show everything wey arrive as dependency. Full dpkg --get-selections for five-year-old box fit return two thousand lines and still tell you nothing about wetin dem intend.
Scheduled work dey hide for two places, so check both. Job wey dey run only monthly na the one wey you go discover six weeks after the migration.
# old server: per-user crontabs, then the system drop-ins
for u in $(cut -d: -f1 /etc/passwd); do sudo crontab -lu "$u" 2>/dev/null | sed "s/^/$u: /"; done
sudo ls -la /etc/cron.d /etc/cron.daily /etc/cron.hourlyThen check the parts wey no be ordinary files: firewall rules, certificates, databases, and how much data you really dey move.
# old server
sudo ufw status verbose # or: sudo nft list ruleset
sudo certbot certificates
sudo -u postgres psql -c '\l' # or: sudo mysql -e 'SHOW DATABASES;'
sudo du -xh --max-depth=1 / | sort -hcertbot certificates go print each certificate name, the domains wey e cover, the expiry date, and the paths of the files for disk. That output na your TLS checklist. du -x dey stay for one filesystem, so e no go enter mounted backup volume and report number wey big ten times pass the real one.
Two things dey outside the server and people dey forget dem every time. First, any third party wey allowlist your server IP address: payment gateway, managed database, SMTP relay, or partner API. The new box get new address, so add the new IP to those allowlists before cutover, no be after. Second, DNS records wey you no create yourself, like an MX record or SPF record wey mention the old IP for the text.
Why you rebuild instead of cloning the old root filesystem
To clone the whole root filesystem put the new VPS dey look like the faster option, and e really dey faster, until e no be again. Root filesystem wey don dey production for years dey carry config files wey person edit by hand and nobody document, packages from repository wey no dey exist again, and boot setup wey dem build for the old platform virtual hardware. You go import everything, including the reason wey make you migrate.
Rebuilding slow for the first day, but e cheaper every day after that. Install the current release, apply your base hardening, then copy only data: the application directory, the site configs, the database dump, the certificates, and user uploads. Anything wey you no fit explain no go come across. Start the new box the same way you go start any box, with the first ten minutes for a new VPS, then add services one by one from the inventory and confirm each one before you add the next one.
When image or snapshot restore dey make sense
There dey one honest exception to rebuilding. If old server no go boot, or the application na one wey nobody fit rebuild from source again, provider image or snapshot restore na the practical answer. E get real limits: e dey work inside one provider, and often only inside one plan family, because the restored disk dey expect that platform virtual devices and network naming.
Snapshot of a running box still get the same consistency problem like any other file-level copy of live database. Treat image restore as recovery route, no be migration plan, and read why snapshot no be the same thing as backup before you build plan on top of am.
How files dey move: rsync over SSH
Run rsync from the old server, and push the files go the new one. Pushing dey usually simpler because the old server already get the data and fit read everything under sudo.
# old server: dry run first, and read what it says it will do
sudo rsync -aHAX --dry-run --itemize-changes \
-e 'ssh -i /root/.ssh/id_ed25519' \
/srv/app/ deploy@203.0.113.20:/srv/app/
# old server: the real bulk pass
sudo rsync -aHAX --info=progress2 \
-e 'ssh -i /root/.ssh/id_ed25519' \
/srv/app/ deploy@203.0.113.20:/srv/app/The flags matter. -a dey preserve permissions, timestamps, symbolic links and ownership. -H dey keep hard links as hard links instead of expanding dem into separate copies. -A dey copy POSIX ACLs (access control lists), and -X dey copy extended attributes. If you no include those last two, file fit look identical but behave differently, because SELinux labels and ACLs dey inside extended attributes and nothing else dey record dem.
Two details dey cause most failures here.
The trailing slash decide where the data go. /srv/app/ mean the contents of that directory. /srv/app mean the directory itself. If you get am wrong, you go end up with /srv/app/app for the new server. The application go start, then report missing files, because the paths wey you configure for am don become one level too shallow.
Under sudo, the tilde na root's home directory. If you write -e 'ssh -i ~/.ssh/id_ed25519' inside a sudo rsync, e go look for the key inside /root/.ssh, no be your own home directory. If the key no dey there, SSH go print Permission denied (publickey), rsync go print rsync: connection unexpectedly closed and exit with non-zero status. Write the key path in full. If that authentication message still dey appear after you correct the path, the publickey failure get short list of causes and directory permissions for the new server na the next thing to check.
Ownership need one decision. When you run rsync as root, e dey map owner and group by name by default. So, file wey get ownership by www-data for the old server go become owned by www-data for the new server, even though the numeric UID (user ID) different. Na this one you want for a rebuild. Add --numeric-ids only when you dey copy a filesystem whose accounts no dey for the target. Then check the result with ls -ln, because file wey get UID without matching account go show as bare number, and every service wey try read am go get permission denied.
Run the bulk pass days before cutover, while the old server still dey serve network traffic. You fit repeat am as often as you like: rsync dey send only wetin change, so the second pass go take minutes instead of hours. The final pass, inside the cutover window, add --delete so files wey you remove for the old server go also disappear for the new one.
# old server: final pass, inside the window, after the app has stopped writing
sudo rsync -aHAX --delete --info=progress2 \
-e 'ssh -i /root/.ssh/id_ed25519' \
/srv/app/ deploy@203.0.113.20:/srv/app/--delete dey remove files for the destination wey no longer dey for the source. So, wrong source path plus --delete fit empty the destination directory. Run am with --dry-run first, every single time. Long transfers fit also die when your laptop SSH session drop, so start dem inside tmux or screen for the old server. Add --bwlimit=20M if the copy dey saturate the link while the old server still dey serve users.
How database dey move: native dump
Database no be directory of files, even though e fit look like one. E be set of files plus state wey dey memory plus write-ahead log, and e consistent only for the exact moments wey database itself define. Use the tool wey belong to am.
PostgreSQL need two dumps, because roles dey apply across the whole cluster and pg_dump no include dem:
# old server
sudo -u postgres pg_dumpall --globals-only -f /var/backups/globals.sql
sudo -u postgres pg_dump -Fc -f /var/backups/appdb.dump appdb# new server: restore in this order
sudo -u postgres psql -f /var/backups/globals.sql
sudo -u postgres createdb -O appuser appdb
sudo -u postgres pg_restore -d appdb --no-owner /var/backups/appdb.dumpIf you skip globals.sql, every table go restore but no application role go fit read dem, because GRANT statements dey refer to user wey no exist. -Fc dey write custom archive format. Na only pg_restore fit read am, and e let you restore selected tables later. Restore into the same major version or newer one. Going backwards, from 17 to 16 for example, no dey supported. pg_restore go reject the archive with unsupported-version error for the file header before e write anything.
MySQL and MariaDB use one command, with four options wey no be defaults:
# old server
sudo mysqldump --single-transaction --routines --triggers --events \
--databases appdb > /var/backups/appdb.sql# new server
sudo mysql < /var/backups/appdb.sql--single-transaction dey take consistent snapshot without blocking writers, but na only for InnoDB tables. If MyISAM table dey the same database, e go copy am without that guarantee. So check your storage engines before you trust the dump. --routines, --triggers and --events dey off by default. This mean say plain dump go restore your data and silently leave your stored procedures and scheduled events behind. Database users and their grants dey inside mysql system database. --databases appdb dump no dey touch am, so recreate dem for the new server with CREATE USER and GRANT. MariaDB 11 ships the same tool as mariadb-dump and keeps mysqldump as symbolic link, so either name go work as of August 2026.
SQLite na single file, and if you copy am while application dey write, you go get torn file. E get its own safe method:
# old server
sqlite3 /var/lib/app/app.db ".backup '/var/backups/app.db'"No matter the engine, check the dump before you trust am. Dump wey stop early because disk full go still restore without complaint, right up to the point where truncation happen.
# old server: a complete mysqldump ends with a line reading "-- Dump completed on ..."
tail -n 3 /var/backups/appdb.sql
# new server: after restore, count rows in a table whose size you know
sudo mysql -e 'SELECT COUNT(*) FROM appdb.orders;'Why rsync no fit copy database wey dey run
rsync dey copy file one by one. Database wey dey run dey write to several files at the same time, so by the time rsync reach the last file, the first file don already become outdated. The copy go contain pages from different moments, and database never get that kind state. The result fit be server wey refuse to start, or the worse case: server wey start and dey return correct answers for one week, then fail when query finally reach the damaged page. No warning dey show before then.
Two safe ways dey to move the files themselves. Stop the database, copy am, then start am again: this one correct and simple, but downtime go last as long as the copy. Or use the tool wey dem build for physical copy of server wey dey run. For PostgreSQL, na pg_basebackup, and e dey coordinate with the server so the copy go consistent:
# new server: pull a physical copy from the old one
pg_basebackup -h 198.51.100.10 -U replicator -D /var/lib/postgresql/16/main -X stream -PThis one need role wey get REPLICATION attribute and matching pg_hba.conf entry for the old server, so setup dey more than dump. E make sense when database big reach level where dump and restore no fit inside your maintenance window. For normal migration from one server, dump dey better.
Build the certificates again before cutover, no be after
TLS certificate dey tied to domain name, no be IP address, so certificate file itself fit move without wahala. But renewal no dey move cleanly. Certbot default HTTP-01 challenge dey ask certificate authority to fetch file through port 80 for the name wey dem dey certify. Until DNS point to the new server, that fetch go land for the old one, and renewal for the new server go fail.
The first option na to copy the existing certificates and their renewal state. Dem go remain valid until their expiry date, no matter which server hold dem.
# old server
sudo rsync -aHAX -e 'ssh -i /root/.ssh/id_ed25519' \
/etc/letsencrypt/ root@203.0.113.20:/etc/letsencrypt/Every file under /etc/letsencrypt/renewal/ dey name the authenticator plugin wey issue the certificate. So install the same plugin for the new server (python3-certbot-nginx, for example). Otherwise, the first renewal go fail with message about unknown authenticator. Confirm say renewal dey work before you depend on am:
# new server, after DNS has moved
sudo certbot renew --dry-runThe second option na to issue fresh certificate for the new server with DNS-01 challenge. This challenge dey prove control through a TXT record and e no ever touch port 80. E fit work before migration, while the name still dey resolve to the old server. That one make am the cleaner choice if you fit automate your DNS provider. How to issue certificates with DNS-01 challenge explain the plugin and credential setup.
For both options, check wetin the new server actually dey present, without changing DNS:
# your laptop
echo | openssl s_client -connect 203.0.113.20:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -subject -dates -issuer-servername dey send SNI (server name indication). Na this one make web server choose the correct virtual host. If you leave am out, you go get the default certificate for that IP and a mismatch wey go look like serious problem, but e no be.
Lower DNS TTL days before cutover
DNS na where careful migration still fit fail, because delay dey built inside and you no fit shorten am on the day. Resolver wey don cache your A record go continue serve am for the length of the TTL (time to live) wey e receive. Lowering the TTL now no go change anything for resolver wey cache the record ten minutes ago with the old value: e go hold the old value for the remaining part of the old TTL, and na after that e go learn the new, shorter one. So lower the TTL at least one complete old-TTL period before cutover. One day before na the safer option. If these moving parts still new to you, the walkthrough about records, resolvers and caching go explain the background.
The numbers below na arithmetic from the TTL itself, no be measurement.
The data behind this chart
[
{
"label": "TTL 3600 s (common default)",
"ttl_seconds": 3600,
"worst_case_stale_minutes": 60
},
{
"label": "TTL 900 s",
"ttl_seconds": 900,
"worst_case_stale_minutes": 15
},
{
"label": "TTL 300 s (lowered for cutover)",
"ttl_seconds": 300,
"worst_case_stale_minutes": 5
},
{
"label": "TTL 60 s (short window)",
"ttl_seconds": 60,
"worst_case_stale_minutes": 1
}
]Record wey publish with TTL of 3600 seconds fit continue send users go the old IP for 60 minutes after you change am. Lower am to 300 seconds and that worst case go reduce to 5 minutes. Treat these figures as the minimum, no be promise. Some resolvers get their own minimum TTL and ignore anything shorter, while some application runtimes cache resolved address for as long as the process dey run. So client wey start before your change fit no check again until e restart.
Read the authoritative answer, no be your own cache, when you dey confirm say the lower TTL don become active:
# your laptop: ask the zone's own nameserver, so no cache is involved
dig +short NS example.com
dig +noall +answer @$(dig +short NS example.com | head -n1) example.com AThe second field for that answer line na the TTL in seconds. Then check the records wey people dey often forget: the AAAA record if the old server get IPv6, the www name when e be separate A record instead of CNAME, any MX record wey point to the server itself, an SPF record wey list the old IP, and the reverse DNS (PTR) record for the new address. Set the PTR through your provider's control panel before cutover if the server dey send mail, because receiving mail servers dey check am, and missing PTR fit cause rejected mail hours after everything else look correct.
Test the new server with its IP before you touch DNS
You fit test the whole application for the new server while DNS still dey point to the old one. Override the name lookup for one request:
# your laptop: force one name to the new IP, for this request only
curl -sS --resolve example.com:443:203.0.113.20 \
-o /dev/null -w '%{http_code} %{ssl_verify_result}\n' https://example.com/--resolve only change where the connection dey go. The TLS certificate still dey checked against the real name, so this one prove both the certificate and the service. %{ssl_verify_result} print 0 when the chain verify.
To click through the site for browser, override the name for your whole machine by adding one line to /etc/hosts for your laptop, or to C:\Windows\System32\drivers\etc\hosts for Windows:
203.0.113.20 example.com www.example.comThen use the application the same way user go use am. Log in. Load one page wey dey read from the database. Submit one form wey dey write to am. Upload one file and confirm say e land for disk. Trigger anything wey dey send email, then check say e arrive, because outbound SMTP from fresh IP fit cause common surprise. Remove the hosts line immediately you finish. If you leave am there, na so you go spend one hour debugging site wey everybody else fit see perfectly well.
The cutover, step by step
- Days ahead: reduce the TTL, run the bulk rsync, build the new server, and test am behind a hosts override.
- On the day, before the window: add the new IP to every third-party allowlist, and confirm say the new server backup job dey configured and pointed to your repository.
- Open the window: put the application for maintenance mode on the old server so e stop accepting writes.
- Take the final database dump, then run the final rsync pass with
--delete. - Restore the dump for the new server and start the services.
- Test again through
--resolveand the hosts override, including one real write. - Change the A and AAAA records to the new IP.
- Monitor both servers. The old server access log go show who still dey reach there, and the number suppose dey reduce towards zero over the TTL.
- Remove the maintenance page.
- Leave the old server running and untouched for at least one week.
The maintenance mode step na the one people dey skip, and na the one wey protect you. Once the new database don accept one write, rollback mean say you either lose that write or dump the new database and load am back into the old one. A read-only window of a few minutes cheap. Two databases wey both don receive writes fit require days of manual reconciliation.
Di rollback plan
Rollback na one action: change the DNS records go 198.51.100.10. E dey work only because of four things wey you do earlier.
- The old server still dey run with its services up and its data intact. You stop writes for there; you no decommission am.
- The TTL still low, so the way back fast just as the way forward be.
- You add the new IP to third-party allowlists instead of replacing the old one. If you remove the old address, your rollback path go fail for the payment gateway.
- The new server no receive any writes wey you no fit identify, because the only writes so far na your own test transactions.
Before the window open, decide wetin go trigger rollback. Two triggers dey enough: any error wey you no fit diagnose within a fixed number of minutes, and any data loss at all. Write dem down before time. Na this go prevent the hour of guessing wey fit turn ten-minute outage into long outage.
Prove migration work
Migration no finish when site load. Check the things wey fit only fail later.
# new server
systemctl --failed
journalctl -p err -b --no-pager | tail -n 40
sudo certbot certificates
sudo systemctl list-timers --all --no-pagersystemctl --failed reporting 0 loaded units listed na the result wey you want. certbot certificates suppose show the expiry dates wey you expect, and list-timers suppose show every scheduled job from your inventory with real next-run time, no be blank.
Then reboot the new server once, on purpose, while you dey watch am. Service wey person start by hand and never enable go work perfectly until the first unplanned reboot for 3 in the morning.
# new server
sudo reboot
# your laptop, once it comes back
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/If application dey run for containers, the same trap get different form, because compose stack need explicit restart policy to come back after reboot.
The last check na the one wey easiest to postpone and wey important pass: the backup job. Migration wey end with server wey no get backup don exchange one risk for another. Run backup for the new box by hand, then restore one file from am into temporary directory. restic repository with restore wey you don actually test na the version of this wey go help when you need am. If you dey run the old and new servers side by side for one week, consistent way to reach and configure each host go stop the two from drifting apart while both dey live.
After cutover: di old server and di last items
Keep di old server for one to two weeks. E go cost one month of plan wey you don plan cancel, and na di only rollback wey you get. Then close di remaining work.
- If you reuse di same hostname for your
~/.ssh/configfor di new box, e go give youWARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!for di first connection, because dat name don dey answer with different host key. Clear di stale entry withssh-keygen -R example.comafter you don confirm why e change, no be automatically, because na di same warning interception attack dey look like. Migration na also good time to review which keys fit reach which systems, and na wetin SSH key management for small fleet dey help you do. - Take one final snapshot or backup of di old server, and store am for somewhere wey no dey with di old provider.
- Remove di old IP from monitoring checks, SPF records, and third-party allowlists, for dat order and as di last thing.
- Cancel di old plan only after you confirm say di final copy readable for another place.
FAQ
How long e dey take to migrate server go new VPS?
The outage wey user go notice na usually final database dump, final rsync pass, and when service start, so small application fit take ten to thirty minutes. The full calendar time dey longer, because you need lower DNS TTL at least one old-TTL period before the switch, and one day ahead dey safer. Plan the bulk data copy days early too. E dey run against live server, and if you repeat am later, e go transfer only the things wey change since the last pass.
I fit rsync running MySQL or PostgreSQL database instead of dumping am?
No. rsync dey copy file by file while database dey write to several files at once, so the copy go contain pages from different moments and represent state wey database never get. E fit refuse to start, or start and fail later when query reach damaged page. Use pg_dump with pg_dumpall --globals-only, or mysqldump --single-transaction, or stop the database first, then copy the files. For large PostgreSQL cluster, pg_basebackup dey make consistent physical copy of running server.
How I go test the new VPS before I change DNS?
Override name lookup for your own machine. For one request, curl --resolve example.com:443:203.0.113.20 https://example.com/ go send the connection to the new IP while e still dey check certificate against the real name. For browser testing, add 203.0.113.20 example.com to /etc/hosts for your laptop, click through login, database read, form write, and file upload, then remove the line. To inspect the certificate alone, run openssl s_client -connect 203.0.113.20:443 -servername example.com.
Which TTL I suppose set, and when I suppose lower am?
Lower the A and AAAA records to 300 seconds, and do am at least one full old-TTL period before cutover. Resolver wey cache the record before your change go keep the old value for the remaining part of the old TTL, so lowering am one hour early no go help if the old TTL na 86400. Raise am back to your normal value some days after migration, once the old server access log don quiet.
I suppose copy the TLS certificate or issue new one for the new server?
Either one dey work. Copying /etc/letsencrypt/ go keep the certificate valid until its existing expiry, but you must install the same certbot authenticator plugin for the new server or the first renewal go fail, so run certbot renew --dry-run after DNS switch to confirm. Issuing fresh certificate dey cleaner when you fit use the DNS-01 challenge, because e proves control through a TXT record and e works before DNS point to the new server. You no fit use the HTTP-01 challenge for the new server until DNS don move, because the validation request go reach the old one.