Industry: Multi-tenant web hosting and custom applications (education, healthcare, small business)
Scope: 13 production websites, a custom student-registration platform, two legacy PHP frameworks, 14 DNS zones, and a 16-mailbox email service
Timeline: 6 weeks, three cutover waves, zero unplanned downtime
Outcome: Infrastructure spend cut by roughly 60%, a decade of deferred PHP upgrades cleared, and a monitoring layer that now catches problems before users do
The situation
Our client operated a portfolio of production websites and custom applications that had grown organically for more than fifteen years: WordPress marketing sites, a custom PHP student-registration platform processing real payments daily, two applications on a framework version released in 2013, and email for every business on the portfolio.
The estate ran across five aging virtual servers, a Kubernetes cluster nobody could explain, a third-party email host billing per mailbox, and a payment-processing stack held together by container images that could not be rebuilt. Monthly spend was north of $1,000 across four vendors, and a cost audit found line items like a $1,250/month analytics add-on and a $96/month empty cluster that had never run a workload.
The mandate: consolidate everything onto one managed server, modernize the runtime, cut the recurring spend, and do it without interrupting businesses that take payments and registrations around the clock.
What made it hard
- The code predated the platform it had to land on. Target runtime was PHP 8.x; parts of the codebase were written for PHP 5. Static analysis catches syntax, but the dangerous failures were runtime-only: PHP 8's changed loose-comparison semantics silently breaking query builders, PHP 4-style constructors that simply never ran, and two applications sharing one temp directory that the new server's service isolation split apart.
- Payments could not blink. The registration platform integrates with a hosted payment gateway and webhooks. A migration that dropped a single webhook would strand a paying customer mid-enrollment.
- Email had real humans on the other end, including users in their 80s who had never edited a mail setting in their lives. “Send everyone new settings and hope” was not an acceptable plan.
The approach
Three waves, each reversible
Rather than one big weekend, we cut over in three waves, each with the previous environment kept warm as a rollback target until the new one had days of verified production traffic. Every wave followed the same discipline: stage from remediated code, run a written regression plan, cut DNS with 5-minute TTLs, verify with real transactions, then and only then decommission.
Modernization measured, not assumed
A grep-based estimate said ~115 files needed PHP 8 remediation. Actual measurement (linting plus a runtime harness that renders every admin and student page as a real logged-in user) put the true surface at 9 files in one app, and surfaced five runtime bug classes no linter reports. After cutover, the same harness cleared 10,384 accumulated error-log entries across 387 distinct warning signatures down to zero, verified page-by-page rather than by hoping.
An email migration designed around the least technical user
The 16-mailbox email move is where most of the engineering went, because the failure mode is a locked-out human, not a down server:
- Identical credentials on both systems, so a user who did nothing kept working.
- Per-address routing pins: anyone not yet switched had their mail transparently routed back to the old host and mirrored to the new one every 15 minutes. Nobody could lose a message no matter what they did or didn't do.
- A personal magic-link page for every user: their password, their settings, a one-tap Apple configuration profile that installs the account with zero typing, and autodiscover so Outlook reconfigured itself from a password prompt alone.
- A live status board showing, per person: whether their device had actually connected to the new server (the only signal that counts when passwords match on both sides), which mail app and OS they use (detected from IMAP client announcements and message-header fingerprints), whether they had opened their personal page, and a running log of who switched when. Support calls went from “what do you see on your screen” to a one-glance answer and a device-specific script.
- Deliverability done before the first message moved: SPF, DKIM signing, DMARC, reverse DNS, and a blocklist delisting, so the new server's first day of sending landed in inboxes.
Monitoring that assumes things will break
Consolidation concentrates risk, so the new server got a defense layer the old fleet never had: a database watchdog that alerts on slow statements and kills runaways before they stall logins, a one-minute HTTP probe on the critical apps, SSL-expiry and payment-webhook health checks (the old environment had silently lost a payment webhook for two days once; that class of failure now pages within 24 hours), and a self-healing mail watcher that restarts failed services, drains stuck queues, and emails a plain-English summary of what was wrong and what it fixed.
Results
| Metric | Before | After |
|---|---|---|
| Servers | 5 VPSes + a Kubernetes cluster + external email host | 1 managed server |
| Recurring infrastructure spend | $1,000+/month across 4 vendors | Roughly 60% lower, one vendor |
| PHP runtime | 5.x/7.4-era code, unsupported | 8.1/8.2, error log verified clean |
| Accumulated runtime warnings | 10,384 entries, 387 signatures | 0 |
| Unplanned downtime during migration | n/a | 0 |
| Payments lost during migration | n/a | 0 |
| Email messages lost during migration | n/a | 0 |
| Billing surprises found in audit | $1,250/mo add-on, $96/mo empty cluster | Cancelled |
| Migration-driven support burden | Every user a phone call | Most users self-served from their personal page; the rest got a one-glance, device-specific answer |
What we'd tell anyone attempting this
- Measure the modernization surface, don't grep it. The 115-file estimate was off by an order of magnitude, and the real bugs were ones lint cannot see. A runtime harness that renders real pages as a real user is worth building on day one.
- Same-credential migrations hide their own progress. When the old and new systems accept the same password, “it works for the user” tells you nothing about whether they moved. Instrument the only signal that matters: real client connections to the new system.
- Route back, don't cut off. Per-user reversibility (pins plus mirroring) turned a scary flag-day into a rolling migration where the slowest user could take a week and lose nothing.
- Audit the bill, not just the servers. Two of the biggest savings had nothing to do with hardware.
- The last 10% is people, not systems. One-tap profiles, personal pages, and a status board that tells you which mail app someone runs before you call them: that's what made the difference between a migration and a month of angry phone calls.
