Skip to main content
Upgrading is a tag change plus a migration run. A new image can carry pending schema migrations; a one-shot runner applies the chain before the app services start, and each container refuses to serve on an out-of-date database (the boot gate). The image carries no state, so the data volumes are never touched — only the app containers are replaced.

Upgrade the Compose bundle

1

Snapshot the database first

A migration rolls forward only — there are no down-migrations. Before an upgrade that carries schema changes, take a Postgres snapshot (a managed provider’s point-in-time restore, or pg_dump) so a bad release is a restore away. Recovery is restore-then-re-migrate, never a rollback.
2

Pin the new tag

Set TAI_VERSION in .env to the release you are moving to.
TAI_VERSION in .env
3

Pull it

4

Run the migrations

The one-shot db-migrate service runs tai db migrate, which applies every pending migration across every component — the core chain and each installed plugin that owns one — in order, recording each in the tai_schema_history table. It is idempotent: a run with nothing pending reports so and exits 0. Preview a run first with docker compose run --rm db-migrate db migrate --plan, which lists the pending files without touching the database.
5

Recreate the app services only

--no-deps recreates the three app services against the new image without restarting Postgres or Redis, so the datastores — and their volumes — stay up.
Read the release notes before pinning. A release that needs a step beyond the migration run — a data backfill, a manual expand/contract cutover — names it.

The boot gate

Each serve and backend container checks, once at startup, that every schema chain its configured features and plugins own is fully applied. A pending migration or a rewritten (checksum-mismatched) chain is a loud refusal, not a half-migrated serve:
The fix is the message: run the migration step above, then let the container restart. If you recreate the app services before running db-migrate, they refuse to boot with exactly this message — run the migrations and they come up. Running db-migrate before the app services (the step order above) avoids the refusal entirely.

Runtime-installed plugins

An image upgrade replaces the core only. Plugins added at runtime through the marketplace live in the server’s plugin prefix (TAI_PLUGINS_PREFIX), not in the image, so they do not update with a new image tag — a freshly pinned image boots with the same plugin versions the prefix already holds. Update them as a step of their own, after the image upgrade:
This resolves each installed plugin to the newest version compatible with the core you are now running and installs it into the same persistent prefix. A router or middleware plugin — and a channel’s inbound webhook route — comes live on the next reload / config-apply, not on a container restart: the epoch rebuild that a reload runs re-imports the plugin package and its route-sibling modules and re-registers the declared routes, so an upgraded router or middleware is serving once that reload lands.
A plugin that is incompatible with the new core — or otherwise stale — is quarantined loudly at boot: it is not loaded, and the failure is surfaced, not swallowed. A quarantined plugin shows as quarantined in the Studio Installed tab and in tai plugins installed; run tai plugins upgrade --all to move the roster onto core-compatible versions and clear it. Keep the plugin prefix on persistent storage so the installed set survives the container recreation an upgrade performs.

See also