We Swapped the VPS Without Rehearsing the Database Migration cover image
Back to Blog
TechnologyPublished 25 July 2026· Updated 24 August 2026· 8 min read

We Swapped the VPS Without Rehearsing the Database Migration

A 2 AM migration that skipped rehearsal cost us six hours of downtime. Here is the playbook we now force every startup through before touching DNS.

We Swapped the VPS Without Rehearsing the Database Migration

The Incident: A Migration That Skipped Rehearsal

The Setup: A WordPress Site on a Budget VPS

Last month we inherited a WooCommerce site running on a single 2GB VPS in Mumbai. The stack was the usual budget combo: Ubuntu 20.04, Apache, PHP 7.4, MariaDB 10.3. No Redis, no OPcache, no monitoring. The client had signed a renewal notice that doubled the monthly price from $12 to $24. They wanted out by the end of the week.

We had done a dozen VPS swaps before, but never under a hard deadline with no rehearsal window. That was mistake number one.

The Trigger: An Unexpected Renewal Price Hike

The renewal email landed on a Tuesday at 4:30 PM IST. The client replied within the hour: "Can you move us by Friday?" We said yes. We should have said no.

The Decision: No Downtime, No Rehearsal, No Problem

At 2 AM IST on Wednesday we told ourselves: we know the steps, we have done this before, let us just do it live. No dry run. No staging subdomain. No TTL shaving. Just dump, transfer, restore, flip DNS.

Spoiler: it took six hours.

What We Tried and What Failed

The Plan We Wrote at 2 AM

The plan was three bullet points in a Slack message:

  1. Dump the database
  2. rsync files to the new VPS
  3. Point DNS and go

No rollback steps. No monitoring. No validation checklist.

The Dry Run That Was Not a Dry Run

We skipped the dry run entirely. We told ourselves the site was small (800 MB database, 3 GB of files). It was not small enough to skip rehearsal.

The Database Dump That Hung the Old Server

At 2:47 AM we ran:

mysqldump -u root -p woocommerce_db > /tmp/wc_dump.sql

The dump ran for 90 minutes. The old server's disk I/O saturated. The site went unresponsive. Customers could not check out. We killed the dump, restarted MySQL, and waited 20 minutes for the server to recover.

The DNS Cutover That Took Six Hours

We updated the A record at 4:15 AM. Because we had not shaved the TTL (it was still 3600 seconds), some resolvers did not pick up the new IP until 10:30 AM. Six hours of split-brain traffic.

The Hardcoded URLs That Broke the Checkout

After the DNS finally propagated, the checkout page returned 500 errors. The database had 1,200 rows with http://old-vps-ip hardcoded in post content and serialized options. We spent 90 minutes running wp search-replace with --dry-run first, then live, fixing serialization errors one table at a time.

The Working Approach: A Rehearsal-First Playbook

Here is the runbook we now follow, adapted from VillageHosting and Think Macro patterns.

Step 1: Inventory and Baseline Checks

We inventory everything: PHP version, extensions, Apache modules, cron jobs, SSL cert paths, email aliases, and any hardcoded IPs. We run:

php -v && php -m
apache2ctl -M
crontab -l

We also check the WordPress config for hardcoded URLs:

grep -r 'http://' /var/www/html/wp-content/ --include='*.php'

Step 2: Shave DNS TTLs to 300 Seconds

We lower the TTL on the A record to 300 seconds at least 24 hours before cutover. As VillageHosting notes, this is the single biggest lever nobody uses. If the TTL is 3600 or 14400, dropping it to 300 means the world finds the new IP in five minutes instead of four hours.

Step 3: Provision the New VPS and Match the Stack

We provision the new VPS with the same OS, PHP version, and extensions. We install the same Apache modules and copy over the virtual host config. We pre-install Let's Encrypt certificates using DNS-01 challenge so SSL is ready before cutover.

Step 4: Dry-Run Migration to a Throwaway Subdomain

We create a subdomain like migrate-client.villagehosting.in and point it at the new VPS. We rsync files, import the database, and load the site. If anything breaks, we find it here, not on cutover night.

Step 5: Incremental Sync with rsync -av --delete

We already have a full copy from the dry run. We do an rsync -av --delete to pull only what changed:

rsync -av --delete /var/www/html/ user@new-vps:/var/www/html/

Step 6: Final Database Dump with mysqldump

On cutover night, we run the final dump:

mysqldump -u root -p --single-transaction woocommerce_db > /tmp/wc_final.sql

The --single-transaction flag avoids locking the tables. We transfer the dump with scp and import it on the new server.

Step 7: Put the Old Site in Read-Only Mode

For WooCommerce, we enable maintenance mode or use a plugin like WP Maintenance Mode. We also disable order processing at the application level to prevent new writes during the final sync.

Step 8: Update DNS and Monitor Propagation

We update the A record and monitor propagation from 10 global resolvers: Mumbai, Bengaluru, Chennai, Singapore, London, Frankfurt, New York, San Francisco, Sydney, and Tokyo. We use dig and nslookup to verify.

Step 9: Validate from Multiple Global Resolvers

We validate that the homepage returns 200, the checkout loads, the cart works, and the admin panel is accessible. We test from multiple geographic locations using tools like curl from different VPS instances.

Step 10: Raise TTLs Back to 3600 and Keep the Snapshot

After 48 hours of clean operation, we raise the TTL back to 3600. We keep the old host's snapshot for 30 days. If anything surfaces later, we can pull from the archive.

Pitfalls We Would Warn an Intern About

Never Skip the Dry Run on a Staging Subdomain

The dry run catches 90% of issues. Hardcoded URLs, missing PHP extensions, broken rewrite rules. All of them show up in staging.

Do Not Trust Default DNS TTLs

A 3600-second TTL means up to an hour of split-brain traffic. Lower it to 300 at least a day ahead.

Watch Out for Hardcoded Absolute URLs in the Database

WordPress stores absolute URLs in post content, serialized options, and widget settings. Always run wp search-replace with --dry-run first.

Do Not Forget Email MX Records During Cutover

MX records do not change when you update the A record, but if the old server was handling mail, you need to set up email forwarding or a temporary SMTP relay. As VillageHosting notes, if the registrar is slow to propagate, set up a temporary SMTP relay from the old host forward to the new host.

Always Test Backup Restore Before Migration

We now test the restore procedure during the dry run. If the backup takes 4 hours to restore, we plan accordingly.

Do Not Cancel the Old Host Until 48 Hours Post-Cutover

We keep the old host running for 48 hours. This catches stragglers, incoming email, and forgotten subdomains.

What We Would Do Differently Next Time

Automate the Inventory and Stack Comparison

We are building a script that compares PHP modules, Apache configs, and cron jobs between the old and new servers. No more manual checks.

Use Replication Instead of Dump and Restore

For larger databases, we would use master-slave replication. As HostMyCode explains, set up the target database as a replica, let it sync completely, then switch application connections with minimal interruption.

Pre-Install Let's Encrypt Certificates on the New Host

We now pre-install certificates using DNS-01 challenge so SSL is ready before cutover. No more scrambling for certs at 3 AM.

Set Up a Temporary SMTP Relay for Email Continuity

We are setting up a temporary SMTP relay on the old host that forwards to the new host. This ensures email continuity during the transition.

Document Rollback Steps and Test Them During Rehearsal

We now document rollback steps and test them during the dry run. If something goes wrong, we know exactly how to revert.

Schedule the Cutover in the 2-3 AM IST Window

We now schedule all cutovers in the 2-3 AM IST window. As VillageHosting notes, most Indian websites see their lowest traffic between 1-5 AM IST. Even with zero-downtime migration, there is always a brief period where some users hit the old server and some hit the new one. Minimising concurrent load during this window reduces the risk of either showing errors.

Lessons Learned

The migration that skipped rehearsal cost us six hours of downtime, a broken checkout, and a very angry client. The migration that followed the full playbook took 12 minutes of actual downtime, with zero errors.

The difference was not skill. It was process.

We now force every startup through the full rehearsal playbook before touching DNS. No exceptions. The 2 AM plan is dead. Long live the 2 AM rehearsal.


Sources:

Enjoyed this article?

Back to Blog