How to Upgrade a Self-Hosted Stack Without Breaking Production
A repeatable upgrade procedure for a self-hosted stack: pinned versions, release notes, verified backups, staging runs, smoke tests and a rollback plan that is a restore, not a hope.

A self-hosted stack does not break when you install it. It breaks six months later, on the morning you run a casual upgrade and discover the database migration is one-way. The fix is not caution or luck. It is a repeatable upgrade procedure: pinned versions, release notes read before every jump, a verified backup, a staging run for anything major, and a written rollback plan that is a restore, not a hope.
This post gives you that procedure. It is written for the stack we deploy and manage most often: Twenty (CRM), Chatwoot (support), Cal.com (scheduling), Plausible (analytics), Listmonk (email), Formbricks (surveys), plus n8n for automation. The examples come from the official upgrade documentation of those projects, because the honest details live there, and they are more interesting than the marketing pages suggest.
Why upgrades are the real cost of self-hosting
When you replace SaaS with open-source alternatives, you trade a monthly invoice for operational responsibility. Setup is a one-time job. Backups and monitoring, once configured, mostly run themselves. Upgrades are the recurring task that never goes away, and they are where self-hosted stacks actually fail.
Three properties of these systems make upgrades risky in ways SaaS never is:
- Database migrations run one way. Listmonk's own upgrade documentation states that a database upgraded by a newer version must not be used with older versions. Rolling back means restoring a pre-upgrade backup, not starting the old binary again.
- Breaking changes arrive quietly. Formbricks v5.2 moved authentication from NextAuth to Better Auth. If you used SSO, you had to re-register the OAuth callback URL at your identity provider before the first restart, or every SSO sign-in failed. That fact lives in a migration guide, not on the pricing page. (Those callback URLs are now pinned, so a future auth-library upgrade will not move them again.)
- The cadence never stops. Twenty ships multiple releases per month. Plausible ships releases its own changelog flags as security updates. Ignore upgrades for a year and you are no longer doing one upgrade; you are doing twelve, blind, with a year of intervening release notes to read.
This is the work. Budget for it honestly and self-hosting is still far cheaper than the SaaS equivalent; we break those numbers down in the real cost of replacing SaaS with an open-source stack. Pretend the work does not exist and you will learn about one-way migrations on a Tuesday morning.
The five failure modes, with receipts
Each of these comes from official documentation. They are not edge cases; they are the normal texture of running this stack.
One-way database migrations
Listmonk's own upgrade documentation is explicit: to downgrade, you restore the pre-upgrade database. The documented procedure drops the public schema and re-imports your pre-upgrade dump. Formbricks is blunter about its v3.0 release: after the data migration runs, there is no downgrade path back to 2.7.2. If you upgraded without a restorable backup, there is no undo.
Mandatory new infrastructure
Formbricks v4.0 made Redis and S3-compatible storage mandatory. A docker compose pull on a Friday afternoon turned into a weekend of provisioning object storage. Infrastructure requirements can change between minor-looking version jumps, and the only place you learn this is the release notes.
Authentication and SSO changes
The Formbricks v5.2 callback-URL problem is the canonical example, but the general pattern matters: anything that touches auth (SSO, 2FA enforcement, session handling) can lock your team out even when the application itself is healthy. Plausible, for instance, added the ability to enforce 2FA for all team members in a recent release. Useful feature, surprising if it arrives unannounced. If you run single sign-on for your stack with authentik, auth-related release notes deserve a second read.
Database engine requirements
Twenty v2.34 requires PostgreSQL 15 or newer. If your instance points at an externally managed database, you must upgrade the engine before the application starts. And PostgreSQL itself is the sharpest tool in the stack: the official documentation is clear that minor releases are drop-in, but major releases change the internal storage format and require a dump-and-restore, pg_upgrade, or logical replication. Moving a postgres:15 volume to a postgres:16 container without a migration does not upgrade your data; it renders it unreadable.
The :latest trap
Many compose files pull image: listmonk/listmonk:latest or equivalent. Formbricks' own migration docs call this out for their Hub component and tell you to pin HUB_IMAGE_REF=:0.8.1 in your .env for a deterministic upgrade. With a floating tag, an upgrade is not a decision you make; it is something that happens to you the next time anyone runs docker compose pull. You lose the ability to say which version you run, which means you lose the ability to roll back.
The upgrade procedure that works
Here is the full procedure. It looks long written out. In practice it is one to two hours per application per quarter, less for patch releases.
Step 1: Pin every version
Every image in every compose file gets an explicit tag: twentycrm/twenty:v2.33.0, not latest. Version pinning is what turns "an upgrade happened" into "we upgraded from X to Y", which is the sentence you need to say when something breaks. Keep the compose files in git so every version change is a commit with a timestamp and a diff.
Step 2: Read the release notes for every version you skip
Not the marketing changelog. The upgrade or migration guide. You are scanning for four things:
- Breaking changes and anything labeled "Migration" (PostgreSQL's own documentation tells you to read this section for every intervening major version, and the advice generalizes).
- New required environment variables or infrastructure (the Formbricks Redis/S3 pattern).
- Auth changes (callbacks, SSO, session handling).
- Minimum versions of dependencies (the Twenty/PostgreSQL 15 pattern).
Check whether the project supports skipping versions. Twenty, from v1.23 onward, supports cross-version upgrades, so you can jump straight to the latest release. Not every project allows this; some require stepping through intermediate majors. The release notes say which.
Step 3: Back up, and know the restore works
The minimum before any upgrade is a database dump and a snapshot of the volumes. Twenty's upgrade guide shows the exact pattern:
docker exec -it <db_container> pg_dumpall -U <postgres_user> > backup_$(date +%F).sql
A backup you have never restored is a hypothesis. The full discipline, including scheduled restore drills, is in our guide to backing up a self-hosted stack and proving it restores. For upgrades specifically: take the backup immediately before, store it off the machine, and confirm the file size is sane before touching anything.
Step 4: Rehearse major upgrades on a copy
For patch and minor releases with additive migrations, a rehearsal is overkill. For anything the release notes flag as breaking, clone the stack. Restore the backup onto a second VPS or a separate compose project, upgrade the copy, and click through it. This is where you discover that the upgrade takes forty minutes instead of four, that a new environment variable is required, or that your reverse proxy config needs a change. The discovery costs you an evening instead of an incident.
Step 5: Upgrade in dependency order
Database engine first, on its own day, if a major PostgreSQL jump is required. Then stateful applications one at a time, never the whole stack at once. Upgrade Twenty on Monday, Chatwoot on Wednesday. If Monday goes badly, Wednesday waits. Upgrading everything simultaneously means that when something breaks, you do not know which of six changes broke it.
For each application the mechanics are usually boring. Listmonk in Docker is three commands:
docker compose down app
docker compose pull
docker compose up app -d
Twenty is: docker compose down, bump the TAG value in .env, docker compose up -d, and the server runs its migrations on startup. Chatwoot on a VM install upgrades through its cwctl CLI. The mechanics are never the hard part. The hard part is everything around them.
Step 6: Run a smoke test, from a checklist
Formbricks' migration docs include the right idea: after upgrading, sign in (including 2FA), create and schedule a survey, and confirm the analytics dashboards render. Write your own equivalent per application and keep it in the repo next to the compose files:
- Twenty: sign in, open a pipeline, create a test contact, send a test email sync.
- Chatwoot: sign in, send a message through each connected channel, confirm notifications fire.
- Listmonk: sign in, send a campaign to a seed list, confirm tracking and bounces register.
- Plausible: confirm the tracking script still reports pageviews on a test property.
- Cal.com: book a test event, confirm the confirmation email and calendar invite arrive.
- n8n: run each production workflow manually once and check executions.
Step 7: Watch the monitors before you call it done
An upgrade is not done when the containers are up. It is done when an hour of normal traffic shows no elevated error rates, no queue backlogs, and no disk or memory drift. If you have not set that up yet, our guide to monitoring a self-hosted business stack covers the signals worth watching.
Worked example: Twenty v2.4 to v2.34
Say you installed Twenty in early 2026 and you are on v2.4. Current release is v2.34 or later. Here is how the procedure plays out with real constraints from Twenty's own upgrade guide. (If you are still evaluating Twenty rather than running it, our walkthrough of migrating from HubSpot to Twenty CRM covers the move itself.)
Reading the notes turns up three things. First, cross-version upgrades are supported from v1.23, so you can jump directly, no stepping stones. Second, v2.5 introduced at-rest encryption of secrets under an enc:v2: envelope; the first boot on v2.5 or later runs backfill upgrade commands that are idempotent but slow, so the first startup will take noticeably longer than usual and that is normal. Third, v2.34 requires PostgreSQL 15 minimum.
Your instance runs the bundled PostgreSQL 14. So this is two upgrades, not one: the engine, then the app. Doing them on the same day violates step 5.
Day one, PostgreSQL 14 to 16. Dump everything with pg_dumpall, verify the dump, stand up a postgres:16 container on a fresh volume, restore, point Twenty at it, and run the stack for a few days. Nothing about Twenty changed, so if something is wrong, it is the database.
Day four, Twenty v2.4 to v2.34. Fresh pg_dumpall to a dated file. docker compose down. Bump TAG in .env to the new version. docker compose up -d. Watch the logs while the migration and encryption backfill commands run; expect the first boot to be slow. Then the smoke test: sign in, pipeline, contact, email.
Rollback plan, written in advance: docker compose down, set TAG back to v2.4, restore the pre-upgrade dump with psql, docker compose up -d. Total rehearsed time: under an hour. Total unplanned downtime if you skipped the rehearsal: unknowable, which is the point.
Should you automate upgrades with Watchtower?
Watchtower polls your registries and restarts containers when new images appear. It is a good tool used in the wrong layer. Automatic updates are reasonable for stateless utilities with no database: an nginx proxy, a uptime monitor, a Discord bot. They are wrong for anything with a schema, because the tool restarts containers but cannot read release notes, take a verified backup first, or notice that this particular release needs a new OAuth callback registered at your identity provider.
If you use Watchtower at all, run it in monitor-only mode against the stateful services so it emails you when new images exist, and reserve automatic restarts for the stateless ones. The notification is the valuable part. The restart is where the risk lives.
Common mistakes
- Running
:latestanywhere stateful. You cannot roll back to "the previous latest". Pin tags. - Upgrading the database engine and the application together. Two changes, one outage, no way to know which one failed.
- Treating the backup as a formality. Listmonk's docs describe downgrades as database restores because that is the only mechanism. No tested backup means no rollback plan.
- Skipping release notes because the jump looks small. Formbricks v5.2 to v5.3 sounds trivial; v5.2 itself broke SSO sign-in for anyone who missed the callback re-registration.
- Upgrading everything at once. Six applications changed, one is broken, and your evening is now a guessing game.
- Ignoring upgrades entirely. The pile does not shrink. A year of deferred upgrades is a weekend project at best and a data-loss incident at worst, especially when projects publish security updates, as Plausible's release log shows it does.
When self-hosting the upgrades is the wrong choice
This procedure is cheap in money but real in time: roughly one to two hours per application per quarter, plus a day or two a year for a database engine upgrade, plus the attention to actually read release notes. For a six-application stack, call it three to four working days a year of competent systems time.
That is a good trade if someone on your team owns it and has the skills. It is a bad trade if nobody owns it. The stack that no one upgrades does not stay frozen; it drifts into unpatched security releases and eventually forces the twelve-upgrades-blind scenario. If you cannot assign an owner, honest options are the vendors' managed clouds (Twenty, Chatwoot, Plausible and Cal.com all sell them, and your data stays exportable) or a managed service where someone runs this procedure for you. What does not work is self-hosting and hoping.
Also note that licensing can shift under you. Cal.com is the sharpest recent example: the commercial codebase went closed-source, and the self-hostable community edition was relaunched as Cal.diy under the MIT license, with teams, workflows, routing forms and SAML removed from it, per Cal.com's v6.4 changelog and the Cal.diy announcement. If you self-host the old Cal.com codebase, an "upgrade" may now mean migrating to a different product with fewer features. Reading release notes includes reading license notes.
Frequently asked questions
How often should I upgrade self-hosted applications? Quarterly is a sensible default for the business stack. It keeps jumps small enough that release notes stay readable, while batching the work into four predictable windows a year. Security updates should go on sooner; most projects flag them clearly in their release notes.
Can I skip versions when upgrading? Sometimes. Twenty supports cross-version upgrades from v1.23 onward, so you can jump straight to the latest. Other projects require stepping through intermediate major versions. The upgrade or migration guide for each project says which, and assuming without checking is how migrations fail halfway.
What is the correct rollback for a failed upgrade? Restore the pre-upgrade database backup and volumes, then start the previous pinned version. Application-level downgrades rarely work because database migrations are one-way; Listmonk's documentation states this directly. If your rollback plan is anything other than a restore, you do not have a rollback plan.
Do I need a staging environment for a small self-hosted stack? Not a permanent one. A throwaway copy, restored from backup on a spare VPS or second compose project, is enough for rehearsing major upgrades. For routine minor and patch releases with additive migrations, the backup plus smoke test checklist is proportionate.
Should I use Watchtower or Renovate to automate upgrades? Use them for awareness, not for autonomous restarts of stateful services. Monitor-only notifications when new images ship are genuinely useful. Automatic container restarts cannot read migration guides, register OAuth callbacks, or verify a backup first, and those are exactly the steps that fail.
How do I keep track of releases across six or more applications? Watch the GitHub repositories for releases-only notifications, subscribe to project changelogs where they exist, and keep one markdown file in your infrastructure repo listing current versions. Review it at the start of each quarterly window. Low-tech, and it works.
Where to go from here
Pick one application from your stack and run the full procedure on it this week: pin the version, read the notes, back up, upgrade, smoke test, write down the rollback. Once it has worked end to end, the other five are repetition. If you would rather this discipline be someone else's job, our managed open-source stack service runs exactly this procedure for Twenty, Chatwoot, Cal.com, Plausible, Listmonk and Formbricks, including the quarterly upgrade windows and the restore drills, and our business automation work covers the n8n layer that ties the stack together.
About AI Agents Plus Editorial
AI automation expert and thought leader in business transformation through artificial intelligence.



