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
/healthreturns OK. - If anything is wrong, restore the pre-upgrade database backup.
Next
- Backups and recovery — back up before every upgrade; it is the rollback path.
- Air-gapped install — the bundle build and transfer.