In July an agency in Bristol asked us to move 340 WordPress sites off a host that had been steadily degrading for a year. They had 4.1 TB of uploads, eleven different PHP versions in use, and a hard constraint: no client was to notice, and no client was to be told in advance.
We finished on the Sunday afternoon with zero customer-facing incidents and two internal ones. This is the runbook, including the parts that did not go to plan.
Thursday: inventory before anything else
The single biggest cause of failed bulk migrations is discovering a dependency after the DNS has already moved. So nothing gets copied until we have a complete inventory, built by reading the source filesystem rather than asking the customer.
$ srvd migrate inventory --ssh legacy-host.example --deep340 sites · 4.11 TB uploads · 812 GB databasesphp 7.4 (18) 8.0 (41) 8.1 (152) 8.2 (118) 8.3 (11)mysql 5.7 (296) 8.0 (44)cron 1,204 entries across 287 sites⚠ 6 sites reference an absolute path under /home/olduser⚠ 2 sites use a plugin with a hard-coded database host⚠ 1 site has a 41 GB uploads directory (video)Those nine warnings are the whole value of the step. Six sites with hard-coded absolute paths would have thrown 500s the moment the document root changed. We fixed them on the source host, on the Thursday, while everything was still live and rollback was free.
Friday: seed the data while the old host still serves
We copy everything twice. The first pass is a bulk rsync that runs for as long as it takes — in this case fourteen hours — with the old host still serving all traffic. The second pass, at cutover, is a delta of whatever changed since. That second pass took under four minutes per site.
- Uploads copied with rsync, checksummed, then verified by count and total bytes per site.
- Databases dumped per-site with consistent snapshots, restored, then row-counted against the source.
- PHP version pinned per site to exactly what the source was running. Nobody gets 'upgraded' during a migration.
- Cron entries recreated but left disabled until after cutover, so nothing fires twice.
Saturday: cutover in batches of forty
TTLs were dropped to 60 seconds on the Wednesday, which is the one thing you cannot do on the day. Then eight batches of roughly forty sites, each with a fifteen-minute verification window before the next started.
| Batch | Sites | Delta sync | Verification | Result |
|---|---|---|---|---|
| 1 | 40 | 3m 12s | 14m | Clean |
| 2 | 40 | 2m 51s | 14m | Clean |
| 3 | 40 | 3m 40s | 22m | 1 rollback |
| 4 | 40 | 3m 02s | 15m | Clean |
| 5 | 45 | 4m 18s | 14m | Clean |
| 6 | 45 | 3m 55s | 14m | Clean |
| 7 | 45 | 3m 31s | 16m | 1 rollback |
| 8 | 45 | 4m 02s | 18m | Clean |
The two rollbacks
In batch three, a site using a commercial booking plugin failed its verification because the plugin phones home to a licence server that allowlists by IP. Our verification caught it — the booking form returned a 402 — and we rolled that single site back inside four minutes. We got the new IP allowlisted on Monday and moved it separately.
In batch seven, a site with a 41 GB video uploads directory had a delta sync that ran long enough for two new uploads to land on the old host after the delta started. Verification caught the byte-count mismatch. Rolled back, re-synced with the site in maintenance mode, moved successfully forty minutes later.
“Both failures were caught by automated verification, not by a human noticing, and not by a client emailing. That is the only part of this runbook I would call non-negotiable.”
What we would do differently
- Put any site over 20 GB into its own batch from the start, rather than discovering the problem at delta time.
- Ask specifically about licence servers that allowlist by IP. 'Do you have any commercial plugins' is too vague a question.
- Keep the rollback window at 14 days rather than 7. Two clients found unrelated pre-existing bugs in week two and it was useful to be able to prove the migration was not the cause.
If you are sitting on a client book you want to move, the process above is what you get, and it costs nothing regardless of how many sites you have. We do them in batches specifically so you can watch the first forty before committing the rest.