Nothing ruins a Tuesday quite like an email that, under all the manners, just says deprecated. Not the angry-client kind — you can handle those. The quieter, worse kind: a platform you have leaned on for years letting you know, very politely, that it is going away.
For a good while now the sites I look after have run on Bitnami’s WordPress image on AWS Lightsail. It was a sensible choice at the time. Bitnami packaged the whole stack, Lightsail made it cheap and predictable, and the two together were boring in the way infrastructure ought to be. Then the ground shifted. AWS moved to shipping its own managed WordPress blueprint, and Bitnami’s long run as the Lightsail WordPress image started winding down. Which left me looking at a fleet of roughly sixty live sites, all sitting on a foundation with a use-by date.
So this is the story of moving all of them — the requirement, the approach, and how it actually turned out. The gory details, the bits where I got it wrong and had to go back, are their own posts. This one is the map.
The requirement, stated plainly
Every Bitnami WordPress instance in the fleet had to end up on the new AWS-managed blueprint — wordpress_ls_1_0, not Bitnami — with its data intact, its certificate reissued, its domain pointed at the new box, and as little downtime as I could manage. Sixty-odd times. Without turning it into a full-time job for a month.
The naive version is a checklist you run by hand. Spin up a new instance. Move the database and the files across. Reissue SSL. Repoint DNS. Stop the old box. Anyone who has done that dance even once knows how it goes on site number fourteen: you forget the search-replace, or you cut DNS over before the cert is live, or you delete the wrong instance because two of them are called something-3. The manual path doesn’t just cost time. It costs attention, and attention is the thing that runs out first.
So the real requirement wasn’t “migrate the sites.” It was “make migrating a site boring enough that I can do ten in an afternoon without holding my breath.”
The shape of the approach
I settled on one principle early and it shaped everything after: one site at a time, attended, and resumable. Not a big-bang script that migrates the whole fleet unattended while I sleep — too much can go sideways per site, and a fleet-wide failure at 3am is a nightmare you don’t want to debug. Instead, a pipeline that does one site properly, tells you exactly where it’s up to, and can be re-run from the exact step that failed.
The work breaks into phases, each a small script that does one job and writes a little JSON summary of what it did:
0 preflight is this actually a Bitnami WordPress site, and is it reachable? 1 provision stand up the new managed instance 2 bootstrap prep the new box, work out the canonical domain 3 export pull the site's data off the old box 4 transfer ship it across via S3 5 import restore it onto the new box 6 verify do the numbers match — posts, users, the raw DB siteurl? 7 swap-ip move the static IP from old to new 8 finalise reissue the Let's Encrypt certificate on the new box 9 stop-old park the old instance (don't delete — not yet)
An orchestrator strings them together and threads each phase’s output into the next. Because every phase records whether it finished, a re-run skips the parts already done and picks up where it broke. That single decision — treat “done” as a file on disk, not a hopeful assumption — is what turned a fragile script into something I trusted to run against a live site.
Three constraints did most of the shaping, and each one is a post of its own later in this series, so I’ll keep them short here.
The first is that Lightsail won’t let you attach an IAM role to an instance the way EC2 does. That sounds like a footnote and it is not — it changes how credentials reach the boxes during the data transfer. The short version: a long-lived IAM user, and the credentials ride onto the instance as environment variables on a single SSH command, never written to its disk.
The second is host keys. A full fleet migration, done naively, would ask me to accept an SSH host-key fingerprint by hand somewhere north of three hundred times. That is exactly the sort of repetitive yes-yes-yes that trains you to stop reading prompts, which is how you eventually type “yes” to the wrong one. So the tooling captures each new box’s fingerprint automatically — it’s already been provisioned through an authenticated AWS call, so trusting it is sound — and threads it through every step after.
The third is that no single way of moving the data fitted every site. The obvious tool works beautifully on a mid-sized single site and falls apart on a huge multisite or a tiny 1 GB box under live traffic. So there are three modes, chosen automatically by the size of the box and the shape of the site. That one took a couple of genuinely bad afternoons to work out, and it gets the fullest telling later.
What running it actually looks like
Discovery comes first. An inventory script scans the fleet against what’s in AWS and classifies every site — ready, needs a bit of config, already migrated, or “no idea, go and look”. You fix up the handful that need it, then hand a batch to the runner:
pwsh -File batch-migrate.ps1 -Slugs pilotsite,acmetours,ridgesite
Each site runs through all the phases in turn, its logs and state landing in its own folder. At the end you get a summary you can actually read:
=== batch summary === pilotsite SUCCESS elapsed 9m 22s acmetours SUCCESS elapsed 11m 04s ridgesite SKIPPED preflight: already on wordpress_ls_1_0 total: 3 sites - 2 success, 0 errors, 1 skipped
A batch is whatever I can babysit in one sitting — three to ten sites, an hour or two. Attended, deliberately. I’m in the room. But I’m not typing the same fourteen commands per site and hoping I got them in the right order.
How it turned out
The first site through the whole pipeline, end to end, was one called pilotsite. I ran it, watched it, found the rough edges, fixed them, and ran it again from scratch to be sure — new instance, fresh export, fresh restore, the lot. When it came out the far side with matching post counts and a valid certificate on the right domain, I finally believed the thing worked. From there it was batches.
The old instances don’t get deleted at the end. They get stopped and left alone for a seven-day grace window, because the one certainty in this work is that you will discover something was wrong two days later, and it is very nice to still have the original sitting there switched off rather than gone. After the week is up, they go.
What surprised me, looking back, is that the deliverable stopped being “the sites got moved” and became the tooling itself. There’s an operator runbook now, a client-facing guide, a per-site audit trail. It turned into a small product for doing a boring job safely, which is more than I set out to build and exactly what the job needed.
It also left a trail of scars worth writing up, because every one of them taught me something. The backup that reported success and wrote nothing. The little box that thrashed itself to death trying to move its own data. The site that went all the way to cutover before I noticed nobody could actually reach it. Those are the next few posts. This was the shape of the whole thing; now we get into where it drew blood.

One thought on “Off Bitnami: migrating a WordPress fleet to managed Lightsail”