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
- Back up Postgres (see Backups and restore).
- Build and push the new images (or mirror them, for an air-gapped install).
- Upgrade:
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.0There 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:
kubectl logs -n critlab deploy/critlab-web -c migrateAuto-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
git pull
pnpm install
pnpm db:migrateThen 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_SECRETfor/api/internal/*. web now authenticates every/api/internal/*route (the sandbox's storage-target lookup, and theassets/gcandnotifications/digestcron endpoints) against the generatedINTERNAL_LOOKUP_SECRET, notAUTH_SECRET. It still falls back toAUTH_SECRETwhen unset, so nothing breaks silently, but any external cron or script callingPOST /api/internal/assets/gcorPOST /api/internal/notifications/digestwithAUTH_SECRETshould switch to theINTERNAL_LOOKUP_SECRETvalue in your Secret (secrets.existingSecret'sINTERNAL_LOOKUP_SECRETkey, or the chart-generated one).