Upgrading
How to move between releases: what the version numbers promise, the order the containers have to come up in, and the one thing that is not reversible.
Upgrades are forward-only in practice. Read that sentence before the rest of the
page: there is a make migrate-down, it is not a rollback plan, and the thing
that gets you back to yesterday is a restore.
What a Version Number Promises
Releases are tagged vMAJOR.MINOR.PATCH and the changelog
is the record of what changed in each.
| Change | Lands in | What it means for you |
|---|---|---|
feat: | a minor release | New capability. Migrations likely. |
fix:, docs:, perf: | a patch release | No new schema in the normal case. |
| Breaking change | a minor release, called out at the top of its changelog entry | Read the entry before upgrading. |
The HTTP API is versioned separately under /api/v1 and does not move with the
release number. A minor release does not remove an endpoint or a response field.
The documentation on this site records which release it was last checked against, at the foot of every page. If that lags the release you are running, the drift is in the docs rather than in your install.
Before You Start
Read the changelog entry for every release you are skipping
Not just the one you are moving to. Going from
v1.21.0tov1.24.0means the breaking changes in1.22and1.23apply to you too, and each is called out in its own entry rather than repeated in the latest one.Take a backup, and know that you can restore it
bash docker compose exec -T postgres \ pg_dump -U burnerbyte -d burnerbyte --format=custom --no-owner \ > pre-upgrade-$(date +%Y%m%d).dumpBackup and Restore is the full procedure, including the object store. For a patch release the dump alone is usually enough; for a minor release take both.
Note the version you are on
bash curl -s localhost:8080/api/v1/admin/version -H "Authorization: Bearer $TOKEN"Or read it off Settings → System. You need it to know which changelog entries apply, and to know what to go back to.
Docker Compose
git fetch --tagsgit checkout v1.24.0docker compose build --build-arg VERSION="$(git describe --tags --abbrev=0)"docker compose up -ddocker compose up -d runs the migrate service to completion before api and
smtpd start, because both declare service_completed_successfully on it. That
ordering is the upgrade: schema first, then the binaries that expect it.
Confirm the migration step actually succeeded rather than assuming it:
docker compose logs migrate | tail -20migrate Exited (0) is success. A non-zero exit leaves api and smtpd
un-started rather than running against a half-migrated schema, which is the
behaviour you want — but it means "the site is down" and "the migration failed"
look the same from outside.
The build context excludes .git, so an image built without --build-arg VERSION=... stamps dev and Settings → System stops telling you anything
useful. See Monitoring.
From Source
git fetch --tags && git checkout v1.24.0make migrate-upmake buildRestart api and smtpd yourself, however you supervise them. Same order:
migrations, then binaries.
Version Skew
During a rolling restart the two binaries are briefly on different releases, and the schema is already on the new one. That is safe in one direction only: migrations add before they remove, so an old binary against a new schema keeps working, while a new binary against an old schema does not.
Which gives the rule: migrate first, always, and never start a new api
against a schema that has not been migrated. Compose enforces this for you.
From source, nothing does.
If you run more than one smtpd, restart them one at a time — inbound mail is
retried by the sending server, so a few seconds of refused connections costs
nothing. Remember that only smtpd scales out; see
Architecture.
Rolling Back
It reverses one migration, and a down migration that drops a column drops the data in it. It exists for developing a migration, not for recovering a release. Running it on a production database to undo an upgrade is how you turn a bad deploy into a bad restore.
To go back to a previous release:
- Check out the old tag and rebuild, without running migrations.
- If the new release added only additive migrations — new tables, new nullable columns — the old binaries run against the new schema without complaint, and you are done.
- If it did not, restore the dump you took before the upgrade. That is the only path back, and it costs you everything written since.
The changelog entry says which of those two you are in. If it does not say, assume the second.
Verifying
Beyond "the pages load":
curl -fsS localhost:8080/healthz && echo okcurl -fsS localhost:8080/readyz && echo ready/readyz is the one that matters: it fails while the database or Redis is
unreachable, which is what a half-finished upgrade looks like.
Then send one mail to a live inbox and watch it arrive without refreshing. That
exercises smtpd, the database, the object store and the WebSocket bridge in a
single action — which is most of what an upgrade can break.