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.
Author
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:
- Dump the database
- rsync files to the new VPS
- 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:
-
How we do zero-downtime migrations (at 2am, while you sleep) - VillageHosting.in
-
Database Migration Strategies for VPS Hosting in 2026 - HostMyCode
-
How to Migrate from Shared Hosting to VPS Without Downtime in (2026)? - A2Z Web Help
-
How to Migrate from Shared Hosting to VPS Hosting in India - YouStable
Sources
Related reading
Enjoyed this article?
Back to Blog


