Upgrading Strapi from v4 to v5 in production
Strapi v5 is a breaking major version, with a new data-access API at the core of it. Here is how I upgraded a production CMS from v4 to v5 without losing content or downtime: a stepwise path, 19 lifecycle hooks rewritten, a cloned database to rehearse on, and a cutover that kept production for last.
Major-version upgrades of a stateful, content-bearing app are where "it is just a version bump" goes to die. Strapi v5 changes the data-access model, so the upgrade is not only new dependencies, it is rewriting how the application reads and writes its own content. The content in the database had to survive, and the site could not go down. This is how I did it.
Step up through the latest v4 first
The first mistake would have been jumping straight from an old v4 to v5. I upgraded the codebase in steps: old v4, then the latest v4, then v5. Landing on the latest v4 first clears the deprecations and surfaces the breaking changes while you are still on a familiar major version, so the final jump to v5 is the only thing left to debug. The same pass moved the runtime to Node 20 and switched the database driver to the maintained one.
The real breaking change: the Document Service
Strapi v5 replaces the old entity-level data API with a new Document Service, and that touches every piece of custom logic. The heaviest part of the migration was rewriting all 19 lifecycle hooks to the new API. Lifecycle hooks are the custom behaviour that runs when content is created, updated or deleted, so getting them wrong is how you silently corrupt data. Each one was migrated and checked individually.
Rehearse on a clone, not on production
Before touching anything live, I cloned the development database into an isolated v5 schema and ran the whole migration against that. The data migration either works on a copy of real content or it does not; finding that out on a throwaway clone is free, finding it out in production is not.
Cut over in phases, production last
The v5 build ran as a new flow alongside the existing v4 one. I cut over environment by environment, starting with the lowest-stakes (sandbox / legacy), validating each before moving on. Production was prepared and then held, cut over only after explicit sign-off. At no point was there a single big-bang switch with no way back.
The upgrade also made it more resilient
Working this closely with the app surfaced latent fragility: the pods would crash-loop when the CMS upstream had a brief blip, and a burst of traffic to one endpoint could saturate it. I added a shared error handler across every request handler and hardened the database connection pool against transient network blips, plus uptime alerting. The upgrade left the service not just newer, but harder to knock over.
What I would tell someone starting this
- Step up through the latest minor of your current major before the big jump. Clear the deprecations where the ground is familiar.
- Treat the data-access API change as the real work. For Strapi v5 that is the Document Service and every lifecycle hook, not the dependency bump.
- Rehearse the data migration on a clone of real content. A copy tells you the truth for free.
- Cut over in phases and keep production for last, behind an explicit go. Run the new flow beside the old one so there is always a way back.