Skip to content
v0.3

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.

In the directory of your instance:

Terminal window
$EDITOR forge.lock # the new version and digests, from the release
source ./env-source.sh
docker compose run --rm kernel node dist/migrate.js # forward-only, idempotent, transactional
docker compose up -d

Migrate 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 migrate again; it is idempotent.
  • A registered tenant with no schema. Run platform.tenant.provision for it, then migrate again. Up to v0.3.3 such a tenant’s migrations were written into public. 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:

Terminal window
docker compose up -d --force-recreate caddy

Take 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:

WhereWhat it saysWhat 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.

Terminal window
git checkout <previous commit> -- forge.lock # or restore the file however you keep it
source ./env-source.sh
docker compose run --rm kernel node dist/migrate.js # answers "up to date": a ledger ahead of it is accepted
docker compose up -d # never `down -v` — that deletes your volumes

A 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; migrate answered “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 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.