Skip to content

Self-hosting

Upgrades and migrations

How database migrations run, and the order to upgrade in.

An upgrade is: back up, migrate the database, roll out the new images. The Helm chart does the middle step for you.

How migrations run

Database migrations are plain SQL files shipped with packages/db (packages/db/drizzle/), applied in order by the migrate image — the same code the new release runs. Each applied migration is recorded in the database, so running the job twice is harmless: it applies only what is missing and exits.

Migrations only move forward. There are no down-migrations, so a downgrade means restoring the database backup you took before upgrading.

Upgrade with Helm

  1. Back up Postgres (see Backups and restore).
  2. Build and push the new images (or mirror them, for an air-gapped install).
  3. Upgrade:
bash
helm upgrade critlab charts/critlab -n critlab --reuse-values \
  --set web.image.tag=0.3.0 \
  --set sandbox.image.tag=0.3.0 \
  --set realtime.image.tag=0.3.0 \
  --set migrate.image.tag=0.3.0

There is no migration hook. The first new web pod to start runs the migration in its migrate init container — waits for Postgres, takes a lock, applies what's pending — before it serves any traffic, while the old pods keep serving; new sandbox pods wait for the schema the same way. If a migration fails, that pod stays Init:Error/CrashLoopBackOff and the rollout stalls there rather than replacing the still-serving old pods. Check it with:

bash
kubectl logs -n critlab deploy/critlab-web -c migrate

Auto-generated secrets and the bundled Postgres password are read back and kept on upgrade, never regenerated.

Keep all four image tags on the same version: web and sandbox must agree on how prototype URLs are signed, and every service reads the same schema.

Upgrade a source checkout

bash
git pull
pnpm install
pnpm db:migrate

Then restart web, sandbox and realtime.

Before you upgrade

  • Read the release notes for new required environment variables. The configuration reference always reflects the current code.
  • Upgrade one version at a time if you skipped several; migrations are cumulative, so skipping works, but one step at a time makes a failure easier to pin down.
  • The licence key is not in the database and is unaffected by upgrades or restores.
  • INTERNAL_LOOKUP_SECRET for /api/internal/*. web now authenticates every /api/internal/* route (the sandbox's storage-target lookup, and the assets/gc and notifications/digest cron endpoints) against the generated INTERNAL_LOOKUP_SECRET, not AUTH_SECRET. It still falls back to AUTH_SECRET when unset, so nothing breaks silently, but any external cron or script calling POST /api/internal/assets/gc or POST /api/internal/notifications/digest with AUTH_SECRET should switch to the INTERNAL_LOOKUP_SECRET value in your Secret (secrets.existingSecret's INTERNAL_LOOKUP_SECRET key, or the chart-generated one).