Skip to content
Kutrion
Browse docs

self-hosting

Upgrading

Upgrade a connected install with git pull and a rebuild, or an air-gapped install by rebuilding the bundle — migrations run on boot; back up first.

Updated Oct 3, 2026

Upgrading Kutrion means moving to a newer image and letting migrations run on boot. The procedure differs only in how the new image reaches the host: pulled and rebuilt on a connected install, or carried in a bundle for an air-gapped one. Either way, take a database backup first.

Back up first

Migrations are forward-only — there is no down-migration. The database backup is therefore both your safety net and your rollback path: if an upgrade goes wrong, you restore the pre-upgrade dump. Do not skip it. See Backups and recovery for the pg_dump command and the full list of what to back up (the database, .env, and the db-data volume).

Connected upgrade

On a host with the repository and network access, pull the new code and rebuild:

cd deploy/onprem
git pull
docker compose up -d --build        # rebuilds the app image; migrations run on boot

The app container applies any pending migrations on boot, then recreates with the new image. There is no separate migration step. Watch the logs to confirm migrations applied and the server started:

docker compose logs -f app
curl -fsS http://localhost:4000/health

Air-gapped upgrade

For a disconnected host, rebuild the bundle on the connected side with a new version tag, transfer it, and extract over the install directory — your .env is preserved.

# On the connected host
cd deploy/onprem
./bundle-offline.sh v1.1.0          # new version tag

# Transfer the tarball, then on the air-gapped host
tar -xzf kutrion-onprem-v1.1.0.tar.gz -C /opt/kutrion
cd /opt/kutrion
./install-offline.sh                # loads the new image and recreates the app container

Extracting over the existing install directory keeps your .env, so you do not reconfigure secrets or URLs. install-offline.sh loads the new image and recreates the app container, which runs migrations on boot exactly as a connected upgrade does. See Air-gapped install for how the bundle is built and transferred.

Releases that narrow elevated reads

Some releases tighten which rows the server’s cross-tenant (elevated) reads may see. Each of those changes ships in two steps: first a release whose code reads the new way, then a later release whose migration narrows the database policy. A server still running code from before the first step would read nothing through the narrowed policy and report it as an empty result, so the narrowing migration checks first.

While it runs, each server writes a marker row (elevation_helper_markers) at boot and every five minutes, naming its host, build and the helper level its code supports. A narrowing migration passes only if a marker written in the last 15 minutes is at the level it needs and no recent marker is below it. The marker is keyed by host name and assumes one server process per host.

Step through releases in order. Install each release in turn and let it start before moving to the next; a normal docker compose up -d --build does this, because the old container’s last marker is minutes old when the new one migrates. If the server was stopped for more than 15 minutes before the upgrade, start the current release once first so it writes a fresh marker.

If the migration refuses, its message names the release to install first and gives the query that finds a host below the required level:

SELECT hostname, build_sha, helper_level, written_at FROM elevation_helper_markers
 WHERE written_at > now() - interval '15 minutes' AND helper_level < 2;   -- the level from the message

Upgrade or stop that host; its row stops counting within 15 minutes.

Skipping releases is an operator decision. If you upgrade across several releases at once, or no server will run against the database before the upgrade, run the named migration once with the bypass, naming yourself as the operator, then start the app as usual:

docker compose run --rm --entrypoint node \
  -e KUTRION_ELEVATION_GATE_BYPASS=0300_example.sql -e KUTRION_OPERATOR="your name" \
  app scripts/migrate.mjs
docker compose up -d

KUTRION_ELEVATION_GATE_BYPASS takes exact migration filenames, comma-separated, and applies only to those files. Each use is recorded in elevation_gate_bypasses with the operator, the database user and the time; a migration that fails leaves no record because the record is part of it. A brand-new database (no users and no markers) needs no bypass.

After upgrading

  • Confirm migrations applied and the server started (the log lines above).
  • Confirm /health returns OK.
  • If anything is wrong, restore the pre-upgrade database backup.

Next