How to Migrate a VPS to a New Provider With Minimal Downtime
A practical VPS migration plan: inventory, DNS TTL, rsync and database cutover, TLS certs, dual-run testing, rollback, and safely decommissioning the old server.
Moving a live server to a new provider is mostly a scheduling problem, not a technical one. The commands are simple; the risk comes from forgetting a cron job, a DNS record with a 24-hour TTL, or a database that keeps accepting writes on the old box after you thought you had cut over. This guide is a repeatable migration plan that keeps downtime in the seconds-to-minutes range for most web workloads, and explains where that goal is unrealistic.
The short version
Minimal-downtime migration follows the same five phases regardless of stack:
- Inventory everything the old server does, including things you did not install yourself.
- Build the new server in parallel and get the application running there with real data, but not yet public.
- Lower DNS TTLs days in advance and test the new host via
/etc/hostsoverrides. - Cut over: freeze writes, final delta sync, switch DNS or IP, verify.
- Dual-run and decommission: keep the old server reachable but read-only for a few days, then wipe and cancel.
The only genuinely hard part is step 4, and only if you have a database or user-uploaded files that change constantly.
Phase 1: Inventory the old server properly
Most failed migrations are caused by something nobody documented. Before touching the new box, collect:
# Services that actually listen on the network
ss -tulpnH | awk '{print $1, $5, $7}'
# Enabled units (including timers)
systemctl list-unit-files --state=enabled
systemctl list-timers --all
# Cron jobs for every user
for u in $(cut -d: -f1 /etc/passwd); do crontab -l -u "$u" 2>/dev/null | sed "s/^/$u: /"; done
ls -l /etc/cron.*/ /etc/cron.d/
# Packages you installed on top of the base image
apt-mark showmanual # Debian/Ubuntu
dnf repoquery --userinstalled # RHEL family
# Non-default sysctl, limits, and mounts
grep -rv '^#' /etc/sysctl.d/ /etc/security/limits.d/ 2>/dev/null
findmnt -t ext4,xfs,btrfs
Also write down the things that live outside the filesystem: DNS provider and record set, TLS certificate issuance method, external API keys allowlisted by source IP, webhook endpoints registered with third parties, SMTP relay credentials, and any monitoring or backup agents phoning home.
The IP-allowlist item is the most common surprise. Payment processors, partner APIs, database-as-a-service firewalls, and some registrar APIs pin your outbound IP. Every one of those needs updating with the new address, and some take days to approve changes. Find them before you schedule the cutover.
Phase 2: Build the new server in parallel
Do not clone the old disk if you can avoid it. A fresh OS install with your configuration reapplied is faster to trust, drops years of accumulated cruft, and lets you move to a current distribution release at the same time. Use the migration as an excuse to codify the build — even a shell script or a short Ansible playbook is better than notes.
On the new host, before anything is public:
- Create your admin user, install SSH keys, and disable password authentication. If you want a checklist for this part, see the practical SSH hardening guide.
- Put a firewall in place immediately, not after the app is running. A baseline nftables ruleset that permits SSH from your own networks and nothing else is enough for the build phase.
- Install the application stack and restore configuration.
- Match the old server's PHP/Python/Node/Postgres major versions for the first cutover. Upgrading runtimes and changing hosts in the same window means you cannot tell which change broke things.
If you already run encrypted off-site backups, your restore is your migration rehearsal: pull the latest snapshot onto the new server and see whether it actually comes up. That is the cheapest possible restore drill, and it validates two things at once.
Phase 3: Move the data
Files
For application code and uploads, rsync over SSH, run repeatedly:
rsync -aHAX --numeric-ids --info=progress2 \
--exclude='/var/cache/' --exclude='*.sock' \
-e 'ssh -p 22' \
/var/www/ newhost:/var/www/
-a preserves permissions, ownership, timestamps and symlinks; -H keeps hard links; -AX keeps ACLs and extended attributes (important for SELinux labels and some mail stores); --numeric-ids avoids UID/GID remapping when the two systems have different user tables. Run it once days early, then again the night before, then a final time during the cutover — each pass transfers only the delta, so the last one takes seconds.
Push from old to new rather than pulling, and use a dedicated key for the transfer that you delete afterwards.
Databases
For anything up to a few tens of gigabytes, a logical dump is simplest and safest:
# MySQL/MariaDB — consistent snapshot without long table locks on InnoDB
mysqldump --single-transaction --quick --routines --triggers --events \
--all-databases | zstd -T0 | ssh newhost 'zstd -d | mysql'
# PostgreSQL
pg_dumpall | zstd -T0 | ssh newhost 'zstd -d | psql -f -'
If the dump-and-import takes longer than your acceptable downtime, set up replication instead: configure the new server as a replica of the old one (binary log replication for MySQL/MariaDB, streaming replication or pg_basebackup for PostgreSQL), let it catch up over hours or days, then promote it during the cutover. Downtime drops to the length of a promotion, typically seconds. The trade-off is more moving parts and a replication link between two providers, which you should tunnel over WireGuard or SSH rather than exposing the database port to the internet.
TLS certificates
Copy /etc/letsencrypt (or your cert store) to the new server so it serves valid TLS the moment traffic arrives. Do not rely on issuing fresh certificates during the cutover — ACME HTTP-01 validation needs DNS already pointing at the new box, which is exactly the wrong ordering, and Let's Encrypt enforces per-domain issuance rate limits that a panicked retry loop can hit. Copy first, then let the new server take over renewals and disable the renewal timer on the old one.
If you serve HSTS with a long max-age, remember that browsers will refuse plain HTTP for your domain. A misconfigured TLS stack on the new server is a hard outage, not a degraded one.
Phase 4: The cutover
A week before: lower the TTL on the records you will change to 300 seconds. Verify the change actually propagated (dig +noall +answer example.com) rather than trusting the control panel. Some DNS providers apply TTL changes only after the previous TTL expires, so do this early.
Test before you switch anything. Override resolution locally:
# on your workstation
echo "203.0.113.10 example.com www.example.com" | sudo tee -a /etc/hosts
Then browse the site, log in, upload a file, trigger a background job, send a test email, and check the logs on the new server. Fix everything you find here — it costs nothing.
Cutover sequence:
- Put the application into maintenance/read-only mode on the old server, or stop the web service.
- Stop cron jobs and queue workers on the old server so nothing writes after the snapshot.
- Final
rsyncdelta plus final database dump/import, or promote the replica. - Start the application on the new server, still without public DNS, and smoke-test through your
/etc/hostsoverride. - Update the DNS A/AAAA records to the new IP.
- Remove the
/etc/hostsoverride and confirm real resolution. - Re-enable cron and workers only on the new server.
Keep the old server running with the app disabled. Clients with stale DNS caches, hardcoded IPs, or aggressive resolvers will keep hitting it for hours. A 503 from the old host is annoying; the old host silently accepting writes into an orphaned database is a data-loss event you will discover a week later.
Rollback plan: as long as the old server still has its data and you have not started writing to it again, reverting is just another DNS change plus reversing the final data sync. Decide in advance what triggers a rollback — a specific error rate, a broken payment flow — so you are not debating it under pressure.
Phase 5: Decommission safely
Wait at least until your old TTL plus a couple of days have passed and traffic to the old IP has stopped. Check its access logs before pulling the plug.
Then, on the old server: rotate every credential that was ever stored there — database passwords, API tokens, SMTP credentials, SSH keys you copied around during the migration. Revoke the temporary transfer key. Delete backups that are no longer needed and remove the old host from monitoring.
If you are leaving a provider you would rather not have holding your data, remember that you cannot verify deletion of a virtual disk. Reinstalling the OS or overwriting the block device (dd if=/dev/zero of=/dev/vda) removes the data your account can see, but on shared storage you are trusting the provider's reclamation process. The durable protection is having encrypted the data at rest in the first place — which is exactly the scope, and the limits, discussed in the guide to what LUKS actually protects on a remote server. Treat "delete the VM" as hygiene, not as a guarantee.
Where minimal downtime is not realistic
Mail servers. You can move the mailboxes and configuration quickly, but the new IP starts with no reputation, needs a matching PTR record from the new provider, and may sit on ranges some receivers treat with suspicion. Plan a warm-up period, keep the old MX as a lower-priority backup during the transition, and read up on the PTR, SPF, DKIM and DMARC requirements before you assume delivery will just continue.
Anything with hardcoded IPs. Mobile apps shipped with an IP, partner systems that allowlist you, or DNS-less internal integrations require coordination, not clever ordering.
Very large datasets. Multi-terabyte volumes over a constrained uplink are bandwidth-bound. Seed the bulk transfer days ahead and use replication or incremental syncs for the tail.
Stateful services without replication. Some self-hosted applications keep state in a way that resists snapshotting under load. Accept a maintenance window rather than risking inconsistency.
Common mistakes
- Changing DNS first, then migrating. Traffic splits across two servers writing to two databases. Always have the destination fully working before DNS moves.
- Forgetting AAAA records. IPv6-capable clients will keep reaching the old server if you only update the A record.
- Leaving cron running on both machines. Duplicate invoices, double-sent emails, two backup jobs fighting over the same repository.
- Not testing outbound connectivity from the new IP. Some upstream services are unreachable or rate-limited from ranges you have never used before.
- Migrating the firewall as an afterthought. A new server exposed with default rules while you finish "just the app part" is how a migration turns into an incident.
- No monitoring on the new host for the first 48 hours. Disk fill from a misconfigured log path is the classic post-migration failure.
FAQ
Should I clone the disk image instead of rebuilding? Cloning is faster and preserves everything, including problems, and it usually requires provider-side support for image import plus network reconfiguration afterwards. Rebuild when you can; clone when the stack is undocumented and rebuilding is genuinely riskier.
How long does DNS actually take? Well-behaved resolvers honour your TTL. Some ISP resolvers and applications cache longer. Plan for a long tail of stale traffic measured in hours, occasionally days, which is why the old server stays reachable.
Can I avoid downtime completely? For read-heavy sites behind a proxy or with database replication, cutover downtime can be a few seconds — effectively invisible. "Zero" downtime for a single-server stateful app is marketing, not engineering.
What about privacy during a migration between providers? The transfer itself goes over SSH, so contents are encrypted in transit, but both providers see connection metadata, and your old provider retains whatever account and billing data you gave it. If the point of the move is to reduce that footprint, pair the technical migration with a clean account and payment path on the new side — see the Monero payment opsec guide for where the money trail typically leaks.
Takeaway
Write the inventory, build the new server from scratch rather than cloning, lower TTLs a week early, test with a hosts override, and cut over in a fixed order with cron disabled on the source. Keep the old machine alive but write-disabled until traffic to its IP stops, then rotate credentials and wipe it. Done in that order, a VPS migration is a boring twenty-minute maintenance window rather than an outage — and IronBalkans' full-root KVM instances in Romania deploy in under a minute, so the parallel build phase costs you a few euros in crypto, not a scheduling negotiation.
