All systems operational Amsterdam · Paris · Reykjavík +5 Pay with Cryptocurrency
Networking & self-hostingIntermediate30 min readUpdated 2026-09-06

Migrate a VPS to a new host without downtime

Every server moves at least once. The mechanics are the easy part — the failures live in the gaps between three clocks that are not synchronised: DNS, data, and the sessions already in flight. This is how to close those gaps on a Tuesday you chose.

Migrate a VPS to a new host without downtime
On this page
  1. The three clocks, and why “zero downtime” is the wrong target
  2. What the old host keeps after you leave
  3. The TTL clock starts a week before the move
  4. Sizing the new box, and what to check before you commit to it
  5. Step-by-step
  6. Mail does not migrate with the rest of it
  7. Rolling back without making it worse
  8. Decommissioning: rotate, wipe, verify, then cancel
  9. A timeline that works
  10. Frequently asked questions

Every server you run will move at least once. The host raises its prices, or starts asking for a document you would rather not send, or takes an abuse complaint about a neighbour and nulls a whole /24 for an afternoon. The question was never whether you would migrate — only whether it happens on a Tuesday morning you picked, or on a Saturday night somebody else picked for you.

The mechanics are not the hard part. Copying files is rsync, and you already know that. What makes migrations go wrong is timing: three separate clocks run during a move, they are not synchronised, and every real failure happens in the gaps between them. This guide is about the gaps — the DNS TTL you should have lowered a week ago, the database you copied while it was still being written to, the certificate that only exists on a machine you just powered off, and the credentials the old host has been able to read the entire time.

The commands assume Debian or Ubuntu at both ends and a web application with a database, because that is what most people are moving. A Nextcloud, a game server, a bot or a BTCPay instance all follow the same shape — only the data step differs.

The three clocks, and why “zero downtime” is the wrong target

A migration is not one event. It is three timers running at different speeds, and the whole craft is in stopping them from overlapping badly:

  • The DNS clock. From the moment you change an A record, resolvers keep answering with the old address until their cached copy expires. You do not control that clock at cutover time — you controlled it when you set the TTL, days earlier.
  • The data clock. Your last copy of the data is a photograph of a moment. Everything written after that moment lives only on the old machine, and will be lost unless you either replay it or stop it happening.
  • The session clock. Uploads in progress, open WebSockets, a payment callback that is going to arrive in forty seconds from a processor that resolved your hostname two minutes ago. These land on whichever box the sender resolved, not the one you would prefer.

Chasing literal zero downtime means keeping all three running simultaneously, which in practice means running the application on two servers writing to two databases at once. That is a genuinely hard problem, and it is the wrong one to solve for a single VPS. The honest goal is narrower and much easier to hit:

No visitor sees an error, and no write is lost. A ninety-second window where the site is up but read-only satisfies both, and almost nobody will notice it. A live cutover with no maintenance window that quietly loses the last forty minutes of form submissions satisfies neither, and you will find out from a customer.

So the plan is: make the read-only window as short as you can, make it boring, and make it reversible. Everything below is in service of those three things.

What the old host keeps after you leave

This part gets skipped, and it is the reason a lot of people migrate in the first place, so it is worth being precise about. While your server lived on someone else's hardware, that provider was in a position to see:

  • The disk, in full. Unless the volume was encrypted with LUKS and unlocked only by you, the hypervisor could read every byte of it — keys, tokens, database contents, the lot. Even with encryption, the memory of a running VM contains the unlocked key.
  • Every credential the machine used. API tokens in environment files, SMTP passwords, wallet daemon RPC secrets, your SSH public keys and, if you ever pasted one into a rescue console, rather more than that.
  • Whatever the account required. A name, a card, an address, a phone number for the SMS verification, the IP addresses you logged in from. That set does not get smaller when you close the account, and in most jurisdictions the provider is required to keep parts of it for years.

None of this is sinister — it is what running a virtual machine on someone else's metal means, everywhere, including here. What matters is that a migration is the only clean break point you get. The new box starts with fresh keys, fresh tokens and a fresh IP; if you carry the old secrets across, you carry the old exposure across with them, and the break was cosmetic.

So treat every secret on the old machine as compromised-by-default and reissue it during the move. It costs you an hour once. Doing it later, separately, is a job nobody ever gets round to. And if part of the reason you are moving is that the account itself is the leak, paying the new one in Monero against a no-KYC account is the step that makes the break real rather than symbolic — though be honest with yourself about what that does and does not buy you, which is its own subject.

The TTL clock starts a week before the move

Time to live is the number of seconds a resolver is allowed to keep your record before asking again. If your A record has the default TTL of 3600, then at the instant you change it, a resolver that asked one second ago will keep sending visitors to the old server for the next fifty-nine minutes and fifty-nine seconds. With the 86400 that many registrars still ship as a default, that becomes a full day.

Check what you actually have — not what you think you set:

dig +noall +answer example.com A
dig +noall +answer www.example.com A
dig +noall +answer example.com MX
dig +noall +answer example.com NS

The second column of each answer is the TTL, counting down. Lower every record involved in the move to 300 seconds, and do it at least twice the current TTL before your cutover: if the record is on 86400, the change itself takes a day to be universally visible, which is the recursive joke at the centre of DNS migrations.

Two details that catch people:

  • The NS records have their own TTL, usually a long one, and it is set at the registrar rather than in your zone. That only matters if you are also changing nameservers — which you should avoid doing in the same week as a server move. Change hosts, settle, then change DNS providers if you want to. Two variables, two weekends.
  • “DNS propagation” is not a thing. Nothing propagates; caches expire. There is no queue to wait in and no button that pushes an update out faster. The only lever is the TTL, and by cutover time it is already pulled.

While you are in the zone file, write down every record that points at the server's IP address rather than at a name. There is usually one more than you remember: mail, webmail, a bare @, an old staging, the SPF record with an ip4: literal in it, and the AAAA record you added when you first got an IPv6 block and then forgot about. Every one of them needs a plan, and the AAAA is the classic silent failure — you flip the A record, everything looks fine from your laptop, and every visitor with working IPv6 keeps landing on a server you have already wiped.

Sizing the new box, and what to check before you commit to it

Resist the urge to order the same specification you have now. You are buying from the numbers on an invoice, not from what the workload actually uses. Spend two minutes measuring first:

# peak RAM actually in use, not allocated
free -m
# what has been paging, if anything
vmstat -s | grep -i swap
# real disk consumption, biggest first
du -xh --max-depth=2 / 2>/dev/null | sort -rh | head -20
# load relative to core count
uptime; nproc

Two rules of thumb hold up well. If your load average sits below your core count and swap has never been touched, the box is not CPU or memory bound and you can move sideways or down. If the disk is above seventy per cent, size the new one on what you will need in a year rather than what you use today — growing a volume later is a migration in miniature, and you are doing one of those right now.

On our side that maps cleanly: Pup (1 vCPU, 1 GB, 25 GB) runs a static site, a VPN or a small bot; Cub (1 vCPU, 2 GB, 40 GB) is the smallest box that hosts an application plus its own database without paging; Scout (2 vCPU, 4 GB, 70 GB) is the comfortable default for a real site with real traffic; Hunter (4 vCPU, 8 GB, 140 GB) and up is where containers, CI or several services on one box start to belong. All of them are all-NVMe with unlimited traffic, so bandwidth overage — a common reason to move in the first place — stops being a variable. Full ladder here.

Then, before you move a single byte, check three things about the machine you have just been given. Each of them is cheap now and expensive after the cutover:

  • The IP's reputation. A recycled address with someone else's history will get your mail refused and your visitors challenged. Ours are screened and risk-segmented before they are issued, but it is your service on it now, so verify it yourself — the five-minute version is here. Do this before the DNS flip, when the answer still costs you nothing.
  • Reverse DNS. If the box will ever send mail, the PTR record must resolve to a hostname that resolves back to the same IP. Custom rDNS is available on request; ask for it now so it has settled by cutover.
  • The route from where your users are. An mtr from a machine in your users' region tells you more about the choice of jurisdiction than any datasheet. Latency you can measure beats latency you assumed.

Step-by-step

  1. Lower every TTL, days before you plan to move

    This is first because it is the only step with a mandatory waiting period. Everything else can be done in an afternoon; this one cannot be hurried, and skipping it is what turns a five-minute cutover into a two-day one.

    Log in to wherever your zone lives and set the TTL to 300 on every record that will change: the apex A, the AAAA, www, mail, and anything else pointing at an IP literal. Then confirm from outside, because control panels and reality sometimes disagree:

    dig +noall +answer example.com A @1.1.1.1
    dig +noall +answer example.com AAAA @1.1.1.1
    dig +noall +answer www.example.com A @8.8.8.8

    The number before IN is the remaining TTL. Query again a minute later: it should be counting down from 300, not from something larger. If it is still counting down from 3600, the old value is cached and you wait it out — which is exactly why this happens on day minus seven and not on the morning of the move.

    Leave the TTL at 300 through the migration and for a week afterwards, so your rollback is also fast. Put it back to something sensible — 3600 is fine — once the old box is gone.

  2. Inventory the old server before you copy anything

    You are not migrating a disk, you are migrating a running system, and the parts people forget are never in /var/www. They are the cron job that runs on the fourth of the month, the firewall rule added during an incident, the package installed from a third-party repository two years ago. Capture the machine's state as text, and copy that text across with everything else:

    mkdir -p /root/mig
    dpkg --get-selections > /root/mig/packages.txt
    systemctl list-units --type=service --state=running --no-pager > /root/mig/services.txt
    systemctl list-timers --all --no-pager > /root/mig/timers.txt
    crontab -l > /root/mig/cron-root.txt 2>/dev/null
    for u in $(cut -d: -f1 /etc/passwd); do crontab -lu "$u" 2>/dev/null | sed "s/^/[$u] /"; done > /root/mig/cron-users.txt
    ss -tlnp > /root/mig/listening.txt
    nft list ruleset > /root/mig/firewall.txt 2>/dev/null || iptables-save > /root/mig/firewall.txt
    cp -a /etc/hosts /etc/fstab /root/mig/
    du -xh --max-depth=2 / 2>/dev/null | sort -rh | head -40 > /root/mig/disk.txt

    Now read listening.txt line by line and account for every port. That file is the definitive answer to “what does this server actually do”, and it is routinely a surprise — a metrics exporter, a forgotten staging copy, a database listening on a public interface that should never have been.

    Write your exclude list at the same time, so the first sync does not spend an hour on data you do not want:

    cat > /root/mig/excludes <<'EOF'
    /dev
    /proc
    /sys
    /run
    /tmp
    /var/tmp
    /var/cache
    /var/lib/apt/lists
    /swapfile
    /var/lib/docker/overlay2
    /var/lib/mysql
    /var/lib/postgresql
    **/node_modules
    **/.cache
    EOF

    Note that the database directories are excluded deliberately. They get their own step, and copying them here is the mistake step six exists to prevent.

  3. Deploy the new server and harden it before it holds anything

    Order the box, pick the location and take the same OS major version as the old one if you can. Migrating hosts and changing from Debian 12 to Debian 13 in the same operation means that when something breaks you will not know which change caused it. Move first, upgrade later.

    Before anything sensitive lands on it, give the machine the ten-minute hardening pass: key-only SSH, root login disabled, a default-deny firewall, unattended security upgrades. It takes ten minutes now and is genuinely tedious to retrofit around a live service later.

    Then create a key that exists only for this migration, on the old server, so that revoking migration access afterwards never means touching your own login:

    # on the OLD server
    ssh-keygen -t ed25519 -f /root/.ssh/id_migrate -C 'migration' -N ''
    cat /root/.ssh/id_migrate.pub

    Put that public key in /root/.ssh/authorized_keys on the new server, then confirm the direction of travel — old pushes to new, so the credential lives on the machine you are leaving and dies with it:

    ssh -i /root/.ssh/id_migrate root@NEW_IP 'hostname; df -h /; free -m'

    Finally, put the new IP in the old server's /etc/hosts under a name like newbox. Every command that follows becomes shorter and, more usefully, harder to point at the wrong machine at one in the morning.

  4. Check the new IP before you trust anything to it

    You now have an address nobody has used for your service before, and this is the last moment when finding a problem with it is free. Three checks, five minutes:

    Blocklists. Run the address through the multi-RBL checks — the full procedure and how to read the results is here. A hit on a policy list like the Spamhaus PBL is normal for a datacentre IP and means little for web traffic. A hit on SBL or XBL is a real one, and the time to raise it is now, not after your users are on it.

    Reverse DNS. Check what the address answers with today:

    dig +short -x NEW_IP

    If the box will send mail, request the PTR record you want and make sure it matches a hostname whose A record points back at the same IP. Forward and reverse must agree; a mismatch is worse than a generic name.

    Reachability and route. Confirm the ports you need are actually open end to end, from outside, rather than assuming your firewall is the only thing in the path:

    # from a third machine, or your laptop
    nc -vz NEW_IP 22
    mtr -rwc 20 NEW_IP

    The mtr output is the one worth keeping. Loss at the final hop matters; loss at an intermediate hop is usually a router deprioritising ICMP and means nothing. If the latency from your users' part of the world is materially worse than the old host, better to find out now, while changing your mind still costs one order and no data.

  5. Copy the filesystem with rsync — and dry-run it first

    Now the bulk transfer. Do it in two passes: a first full copy days ahead, taking as long as it takes, then short delta passes later that only move what changed. Always dry-run first — the output is a list of exactly what is about to happen, and reading it once has saved more migrations than any other habit here.

    # on the OLD server, dry run
    rsync -aHAXx --numeric-ids --info=progress2 --dry-run \
      --exclude-from=/root/mig/excludes \
      -e 'ssh -i /root/.ssh/id_migrate' \
      /var/www/ root@newbox:/var/www/

    Then the same command without --dry-run. Repeat it for each directory that matters: /etc selectively rather than wholesale, /home, /srv, /opt, and wherever your application actually keeps its uploads.

    The flags earn their place. -a preserves permissions, ownership, timestamps and symlinks; -H keeps hard links; -A and -X carry ACLs and extended attributes, which is what your uploads directory needs if anything ever set them; -x stops rsync wandering into other mounted filesystems. --numeric-ids is the one people leave out and regret: without it, rsync maps ownership by name, and if www-data has a different UID on the two boxes, every file arrives owned by the wrong user in a way that is tedious to unpick.

    Two warnings. Copying /etc wholesale onto a running system will overwrite the new machine's network configuration, its fstab and its SSH host keys — copy the specific configuration you need, not the directory. And save --delete for the final pass only: it is correct for making the new box match the old one exactly, and destructive if you have already created something on the destination.

    When the first full pass finishes, compare the two sides so you know it did what you think:

    du -sh /var/www
    ssh -i /root/.ssh/id_migrate root@newbox 'du -sh /var/www'
  6. Move the database with a dump, never a file copy

    This is the step that decides whether your migration is boring or memorable. A database's data directory is only consistent when the server is stopped. Copy it live and you get a set of files that look fine, transfer fine, and restore into a database that is subtly and permanently corrupt — often without an error until weeks later.

    Dump it properly. For MySQL or MariaDB, --single-transaction is what gives you a consistent snapshot without locking the whole thing:

    mysqldump --single-transaction --quick --routines --triggers --events \
      --default-character-set=utf8mb4 --databases appdb \
      | zstd -T0 > /root/mig/appdb.sql.zst

    For PostgreSQL, the custom format is worth it — it compresses, and it lets you restore selectively if you need to:

    pg_dump -Fc -Z6 appdb > /root/mig/appdb.dump

    Ship it over and restore:

    scp -i /root/.ssh/id_migrate /root/mig/appdb.sql.zst root@newbox:/root/
    # on the NEW server
    zstd -dc /root/appdb.sql.zst | mysql
    # postgres equivalent
    pg_restore -d appdb -j4 /root/appdb.dump

    Then verify, because “the restore finished” and “the data is there” are different claims. Compare row counts on the tables that matter, on both sides:

    mysql -N -e "SELECT COUNT(*) FROM appdb.orders; SELECT COUNT(*) FROM appdb.users;"

    Two things that hide in dumps: the character set and collation, which is where mangled accented characters come from — if the old database is on utf8 rather than utf8mb4, decide deliberately whether the move is also the moment to fix that; and the database users and grants, which mysqldump --databases does not include. Recreate the application's user and password on the new box explicitly, then remember that the connection string in your configuration needs to match.

    If your application uses SQLite, the file is the database and the same rule applies — do not copy it live. Use sqlite3 app.db ".backup /root/mig/app.db", which takes a consistent copy safely.

  7. Bring the stack up and rehearse it behind a hosts-file override

    The new server now has the files and the data. Start everything and test it under its real hostname — while the rest of the world is still happily using the old box. This is the most valuable trick in the whole procedure and it costs one line.

    On your own laptop, add the new server's IP to /etc/hosts (or C:\Windows\System32\drivers\etc\hosts):

    203.0.113.10   example.com www.example.com

    Your browser now resolves the real domain to the new server and nobody else's does. Every URL is correct, every cookie domain matches, every redirect and callback path behaves the way it will after cutover — which is precisely what testing against a temporary new.example.com subdomain fails to catch, because half of what breaks in a migration is hostname-dependent.

    For a single quick check without editing anything, curl can do the same thing inline:

    curl -sSI --resolve example.com:443:203.0.113.10 https://example.com/
    curl -sS --resolve example.com:80:203.0.113.10 http://example.com/ -o /dev/null -w '%{http_code}\n'

    Walk the whole application, not just the front page: log in, submit a form, upload a file, trigger a password-reset mail, load an admin page, hit the endpoint a payment processor calls. Then read the error logs even if everything looked fine — missing PHP extensions, a wrong file mode on a cache directory and a database user that does not exist yet all show up there before they show up on screen.

    Remember to remove the hosts line afterwards. Everyone forgets once, and then spends twenty minutes wondering why the rollback appears not to have worked.

  8. Get TLS working on the new box before you flip anything

    Certificates are bound to names, not to IP addresses, so nothing about a migration invalidates a certificate you already hold. What breaks is issuance: the usual HTTP-01 challenge asks Let's Encrypt to fetch a file over port 80 at the name being certified, and that name still points at the old server. The chicken and egg is the whole difficulty, and there are three clean ways out.

    Copy the existing certificates. Simplest and usually right. The private key and chain move like any other file:

    rsync -aHAX -e 'ssh -i /root/.ssh/id_migrate' \
      /etc/letsencrypt/ root@newbox:/etc/letsencrypt/

    The new server can serve valid TLS immediately, and renewal starts working by itself once DNS points at it. Confirm the timer is enabled there: systemctl list-timers | grep certbot.

    Use a DNS-01 challenge. Proves control of the domain through a TXT record instead of an HTTP request, so it works from a server nothing points at yet. Ideal if you want a genuinely independent certificate on the new box before cutover, and the only option for a wildcard.

    Issue after the flip. Valid, but it leaves a gap where the site is live on the new IP with no certificate, which for anything with HSTS is not a warning your visitors can click through. Only sensible for a brand-new domain nobody has visited.

    Whichever you choose, verify against the new IP directly before the cutover, using the same --resolve trick:

    curl -sSI --resolve example.com:443:203.0.113.10 https://example.com/ | head -1
    echo | openssl s_client -connect 203.0.113.10:443 -servername example.com 2>/dev/null \
      | openssl x509 -noout -subject -dates

    Check the dates and that the subject covers every name you serve, www included. And if you use HSTS with a long max-age, treat the certificate as the one thing that must be right before the flip rather than after it — that header is a promise you already made to every returning visitor.

  9. The cutover: freeze writes, final delta, flip the record

    Ten minutes of actual work, and the only part with a clock on it. Do it in the morning, in your own timezone, on a day you are not otherwise busy. Never on a Friday.

    Freeze. Put the old application into maintenance or read-only mode. Stop the workers, the queue consumers and the cron jobs — anything that writes without a browser attached. From here, nothing new is written on the old box, which is what makes everything after this safe:

    # on the OLD server
    systemctl stop app-worker.service
    systemctl stop cron
    touch /var/www/maintenance.flag

    Final delta. One more rsync, now with --delete so the destination matches exactly, and one more dump. With the first pass done days ago, this moves very little and takes seconds:

    rsync -aHAXx --numeric-ids --delete --exclude-from=/root/mig/excludes \
      -e 'ssh -i /root/.ssh/id_migrate' /var/www/ root@newbox:/var/www/
    mysqldump --single-transaction --quick --routines --triggers --events \
      --default-character-set=utf8mb4 --databases appdb | zstd -T0 \
      | ssh -i /root/.ssh/id_migrate root@newbox 'zstd -dc | mysql'

    Check. Row counts on both sides, one more pass through the application over the hosts-file override, and take the maintenance flag off the new box.

    Flip. Change the A record to the new IP. Change the AAAA record too — this is the single most common way a cutover half-works. Confirm from a resolver you do not control:

    dig +short example.com A @1.1.1.1
    dig +short example.com AAAA @1.1.1.1

    Then watch both machines. Traffic should appear on the new one within a minute or two and fade from the old one over the next five:

    tail -f /var/log/nginx/access.log        # on both, side by side

    Leave the old application in maintenance mode rather than switching it off. A straggler that reaches it sees a polite page instead of a connection error, and you keep the machine available for the rollback that you are probably not going to need.

  10. Watch for a week, then decommission properly

    The migration is not finished when DNS resolves. It is finished when a full billing and cron cycle has gone by without a surprise.

    For the first hours, watch error rates rather than uptime — a server can be perfectly up and returning 500 to a third of requests:

    journalctl -u nginx -u php8.4-fpm -f
    awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

    Then work down a short list, because these are what actually break after a move and none of them announce themselves: cron jobs and systemd timers running on the new box (systemctl list-timers against the inventory you took in step two); outbound mail arriving and not landing in spam; scheduled backups pointing at the new server rather than still archiving the old one; any third party with your IP address on an allowlist — a payment processor, an API, a partner's firewall; and the monthly job that only proves itself on the first of the month.

    Leave the old server running, in maintenance mode, untouched, for at least a week. It is your rollback and your reference copy, and it is worth more than the few dollars it costs. Then close it out in order: rotate every secret it could have read, take one final encrypted archive to a third location, overwrite the application and database directories, run the provider's destroy or reinstall, and only then cancel — after checking the renewal date, because an extra month of rollback insurance is usually the better trade.

    Last, put the TTL back up to 3600 now that you no longer need five-minute reversibility, and update whatever documentation you keep with the new address. Future you, at some later 2am, will be extremely grateful that the runbook matches the server.

Mail does not migrate with the rest of it

If the old server sent mail — even just password resets — treat that as a second, slower migration running alongside the first. Deliverability is a reputation system, and reputation is attached to the IP address and the domain, not to the software you copied across.

Four records decide whether your mail is read or discarded, and three of them contain things that change when the server does:

  • SPF lists who may send for your domain. If yours contains an ip4: literal, it is now wrong. Add the new IP before the cutover and remove the old one a week after — both addresses authorised for a few days costs nothing and covers the overlap.
  • DKIM signs the message with a private key. Copy the key across with the rest of the configuration and the selector keeps working. Generate a fresh key instead and you must publish the new selector and wait for it, so copy unless you have a reason not to.
  • DMARC tells receivers what to do when the first two fail. If you are on p=reject, a broken SPF during the window is not a warning, it is silent deletion. Consider dropping to p=none for the migration week and putting it back after.
  • PTR is the reverse record above. A new IP has a generic one until you ask, and several large providers refuse mail from generic reverse names outright.

The honest expectation: a brand-new IP starts with no reputation at all, which is not the same as a good one. Volume ramps over days, not hours. If mail is load-bearing for you, keep the old server alive and sending for a week after the move rather than switching in one step — and if it is business-load-bearing, a dedicated relay is a better answer than either box.

Rolling back without making it worse

The point of a rollback plan is not that you expect to use it. It is that having one lets you cut over calmly at 10am instead of nervously at 2am, and calm is what actually prevents mistakes.

Your rollback is simple, and it stays simple for exactly as long as the old database is still authoritative: change the A record back. With a 300-second TTL you are recovered in five minutes. That window — between the flip and the first write that only exists on the new box — is your free undo, and it is why the old server stays running, untouched and unwiped, for at least a week.

What ruins it is split-brain: writes landing on both machines. Now neither database is correct, and reconciling them by hand is worse than any downtime you were avoiding. Three habits prevent it entirely:

  • Put the old application into read-only or maintenance mode at the start of the window, rather than trusting DNS to have stopped sending traffic. DNS is a hint; a stopped service is a fact.
  • After the flip, watch the old server's access log, not the new one. Requests still arriving there are your stragglers, and when that trickle stops, the migration is genuinely finished. tail -f /var/log/nginx/access.log is the whole tool.
  • Once you accept the first real write on the new box, rolling back is no longer a DNS change — it is a restore. Decide consciously when you cross that line, and say it out loud if there are two of you.

Set yourself a stop rule before you start: if the new stack is not serving correctly within, say, thirty minutes, you revert the record, take the evening back, and fix it with no clock running. Migrations go badly when people keep pushing forward because turning back feels like failure. It is not; it is the cheap option, and it is available for a strictly limited time.

Decommissioning: rotate, wipe, verify, then cancel

A week after the cutover, the old server is a complete, running, unattended copy of your data on infrastructure you have stopped paying attention to. It is also, at that point, the least-patched machine you own. Finish the job in this order:

  • Rotate everything the old box could read. Application secrets, API tokens, database passwords, SMTP credentials, webhook signing secrets, any wallet RPC password. Assume disclosure, because you cannot prove otherwise. If you generated a fresh SSH key for the migration, this is when the old authorised keys come out.
  • Take one final archive — an encrypted restic or Borg snapshot to a target that is neither machine. You will want it exactly once, six weeks later, for a file nobody remembered.
  • Overwrite the data. On a VPS you cannot verify the physical medium, so do what you can: shred or overwrite the application and database directories, then let the provider's reinstall or destroy routine run. Encryption at rest from the start is what makes this cheap; without it, you are relying on someone else's deletion policy.
  • Verify, then cancel. Confirm the new box has been serving everything for a full week, including the monthly cron nobody thinks about, and confirm nothing on the old box is still resolving. Then cancel — and check the renewal date first, because paying for a spare month of rollback insurance is often smarter than saving eight dollars.

Delete the old account itself last, and only once you are certain. Support tickets, invoices and the odd forgotten sub-service tend to live there, and an account you cannot log into is an awkward place to discover a DNS record you forgot to move.

A timeline that works

Spread over a week, none of this is stressful. Compressed into one evening, all of it is.

  • Day −7. Lower every TTL to 300. Inventory the old server. Order the new one and check its IP, its rDNS and its route.
  • Day −5. Harden the new box. Install the stack. First full rsync, which is the slow one — every subsequent pass only moves the delta.
  • Day −3. Restore a database dump onto the new box and bring the application up. Test the whole thing through a hosts-file override. Fix what is broken while nothing is at stake, because something will be.
  • Day −1. Issue TLS on the new server. Confirm the certificate chain and, if you use it, that HSTS will not turn a small mistake into an unclickable one. Add the new IP to SPF. Re-check every TTL has actually gone down.
  • Day 0, morning. Maintenance mode on the old box. Final delta sync. Final dump and restore. Row-count check. Flip the A record — and the AAAA. Watch both access logs.
  • Day +1 to +7. Old server stays up, untouched. Watch logs, mail and the new IP's reputation. Let the monthly jobs run at least once if you can.
  • Day +7. Rotate secrets, final archive, wipe, verify, cancel.

The single best predictor of a boring migration is that step one happened a week before step five. Almost everything that goes wrong in a server move is a TTL that was still 86400 at eleven o'clock at night.

Frequently asked questions

Can a VPS migration really have zero downtime?

Literally zero, with writes accepted continuously on both machines, requires replication and a shared or clustered database — a genuinely hard problem, and the wrong one for a single server. What is achievable, reliably, is that no visitor sees an error and no write is lost: a short read-only window during the final sync, with DNS already on a 300-second TTL so the switch itself takes minutes. In practice that is one to five minutes of maintenance page, which for almost every site is indistinguishable from zero and vastly safer than the alternative.

How long does DNS propagation actually take?

There is no propagation. Nothing is pushed anywhere — resolvers simply cache your record for the number of seconds your TTL specifies, and ask again when it expires. So the honest answer is “as long as the TTL that was in effect when you made the change”. With a TTL of 300 set a week earlier, essentially everyone is on the new address within five minutes. With the 86400 many registrars still default to, some resolvers keep sending traffic to the old server for a full day. This is why lowering the TTL is step one and not step nine.

Can I keep my IP address when I move hosts?

Not unless you own the address space yourself and can have it announced by the new provider, which means being a RIPE or ARIN member with your own allocation — realistic for a company, not for a single server. For everyone else, a new host means a new IP, which is precisely why you check its reputation and reverse DNS before the cutover rather than after. On our side, addresses are screened against Spamhaus and a hundred-plus other lists and issued from risk-segmented pools rather than recycled from whoever had them last, but verifying it yourself takes five minutes and is always worth doing.

Should I use a disk image or a provider migration tool instead?

If both ends are the same provider and the same hypervisor, an image restore is fine and much faster. Across providers it is usually a trap: the image carries the old machine's network configuration, kernel modules, drivers and hardware assumptions, and you spend the evening debugging a system that boots into a machine that no longer exists. A clean install plus a considered copy of your data and configuration gives you a server you understand, and drops the accumulated cruft of the old one. Migrating is the cheapest opportunity you will ever get to leave things behind.

What about a database that is being written to constantly?

Two options, in ascending order of effort. The simple one is the read-only window in step nine: a --single-transaction dump of a busy but normally-sized database takes seconds to a couple of minutes, and a maintenance page for that long is a fair trade. The thorough one is replication — set the new server up as a replica days in advance, let it stay in sync, then promote it during the window. That reduces the freeze to seconds, at the cost of a genuinely more complex setup. Choose the second only if the first is not acceptable, and never improvise it on the night.

Will moving hosts hurt my search rankings?

Not by itself. Google indexes hostnames, not IP addresses, and a move where the URLs and the content stay identical is effectively invisible. What does cost you is what tends to accompany a bad migration: pages returning 5xx while crawlers are visiting, a certificate error, a robots.txt that arrived from a staging copy with Disallow: / in it, or redirects that quietly changed shape. Check robots.txt, your canonical tags and a handful of real URLs immediately after the flip. If the site serves the same responses on the new IP as the old one, there is nothing to recover from.

Do my TLS certificates still work on the new server?

Yes — certificates are issued for domain names, not IP addresses, so copying /etc/letsencrypt across gives you a valid certificate on the new box immediately. The complication is renewal rather than validity: an HTTP-01 challenge needs the domain to point at the machine doing the renewing, so until you flip DNS, renewal will fail there. Copy the certificates before the cutover, flip, then confirm the renewal timer runs successfully on the new server. If you need an independent certificate before the flip — or a wildcard — use a DNS-01 challenge instead.

Deploy an offshore VPS in about a minute

No-KYC, crypto-paid, all-NVMe. Pick a tier, pay in Monero or any major coin, and get root in roughly 60 seconds.

Fenrir on guard