Upgrade & roll back
An instance runs the images its forge.lock pins. Upgrading is changing that file; going back is changing it
back. What never goes back is the database — and the three places this documentation talks about “going back”
all mean that same thing.
Upgrading
Section titled “Upgrading”In the directory of your instance:
$EDITOR forge.lock # the new version and digests, from the releasesource ./env-source.shdocker compose run --rm kernel node dist/migrate.js # forward-only, idempotent, transactionaldocker compose up -dMigrate first, then start — always. A release can carry migrations the new kernel reads from its first
request. /health grades only the system schema, so a kernel started on an unmigrated tenant schema can still
answer ok and then fail the requests that touch the new tables. Migrations are a one-shot step, never a boot
hook: two kernels starting together must not both migrate.
When migrate names a tenant and exits 1 (next release, see Unreleased), that
tenant is not at the image’s last migration, and the run never prints done over it. Starting the new kernel
anyway is the failure above. It was already broken before this upgrade:
- A migration failed half way. The line names the migrations still pending. Fix the cause it printed and
run
migrateagain; it is idempotent. - A registered tenant with no schema. Run
platform.tenant.provisionfor it, thenmigrateagain. Up to v0.3.3 such a tenant’s migrations were written intopublic. Tables left there by that are not touched by the upgrade.
Read the release’s notes before you adopt. Every release in the changelog that changes something on your box has an Upgrading section, in order of risk: what changes in your store on the first minute, what to change in the files you copied from the template, what is deprecated, and whether you can go back.
After changing a bind-mounted file, recreate its service. A single file mounted into a container — the
caddy/Caddyfile above all — is held by its inode. An editor or a git pull that replaces the file leaves the
running container serving the old one, with no error:
docker compose up -d --force-recreate caddyTake a database snapshot before every bump. It is the only undo the data has — see below.
Going back: an image rollback, never a data rollback
Section titled “Going back: an image rollback, never a data rollback”Three sentences you will read, and they agree:
| Where | What it says | What it means |
|---|---|---|
| Deploy & the fleet | “Forward-only from go-live” | Migrations are applied, never reversed. |
| The instance template’s README | “Rolling back the data is not a thing” | There is no down-migration; a snapshot is the data’s undo. |
| A release’s notes | “Can you go back? Yes, with conditions” | You can run the previous images on the migrated database. |
So going back is an image rollback: you put the previous digests back and start them on the database as it is now, migrations of the newer version included. It works when the previous kernel can read a database a newer version migrated — which is what each release’s “Can you go back” section answers, measured, and lists what you lose while you are on the older version.
git checkout <previous commit> -- forge.lock # or restore the file however you keep itsource ./env-source.shdocker compose run --rm kernel node dist/migrate.js # answers "up to date": a ledger ahead of it is accepteddocker compose up -d # never `down -v` — that deletes your volumesA data rollback — returning the database to what it was before the upgrade — is restoring the snapshot you took. It also discards every order, customer and change written since, which is why it is a disaster-recovery move and not an upgrade tool.
What was measured going back and forth (v0.3.3 ↔ v0.3.2)
Section titled “What was measured going back and forth (v0.3.3 ↔ v0.3.2)”On the reference demo instance, adopting v0.3.3 from the published release alone (2026-10-20):
- v0.3.3 → v0.3.2 and v0.3.2 → v0.3.3 took 32 s each way;
migrateanswered “up to date” in both directions, and nothing written was lost. - The per-store payment offer stopped applying on v0.3.2 (every installed payment app offered everywhere) and applied again on the way forward — the choices stayed stored.
- The storefront’s data cache, which lives in a volume, survived the rollback: on the way forward the front served entries cached before it.
The cache after a long rollback
Section titled “The cache after a long rollback”The storefront_cache volume is not tied to a version. Whatever the older front cached while it was running is
still there when the newer one starts, and is served until its window passes or a write purges it. After a short
rollback that is harmless. After a long one, or when the two versions draw the same data differently, empty the
cache once you are forward again — see clearing it by hand.
See also
Section titled “See also”- Volumes & backups — what a rebirth or a
down -vkeeps and loses. - Deprecations — what a published name promises before it goes away.