Self-hosting
Air-gapped installs
Mirror the images, turn off every outbound call, and transcribe voice notes locally.
critlab runs with no internet access at all. Nothing calls home — not at install, not for licence checks, not for telemetry — and every outbound feature can be switched off. This page lists what to mirror and what to set.
1. Mirror the images
Build critlab's images on a connected machine (see Production install with Helm), export them, and load them into your internal registry:
docker save \
registry.example.com/critlab/web:0.2.0 \
registry.example.com/critlab/sandbox:0.2.0 \
registry.example.com/critlab/realtime:0.2.0 \
registry.example.com/critlab/migrate:0.2.0 \
-o critlab-images-0.2.0.tardocker load -i critlab-images-0.2.0.tar
docker push registry.internal.example/critlab/web:0.2.0The full list, depending on which dependencies you bundle:
| Image | Needed when |
|---|---|
critlab/web (or critlab/web-whisper) | always |
critlab/sandbox, critlab/realtime, critlab/migrate | always |
postgres:16-alpine | postgresql.enabled |
redis:7-alpine | redis.enabled |
chrislusf/seaweedfs:4.47 | seaweedfs.enabled |
amazon/aws-cli:2.32.17 | seaweedfs.enabled (creates the bucket once) |
quay.io/oauth2-proxy/oauth2-proxy:v7.15.4 | iap.enabled |
critlab/renderer | renderer.enabled (critlab's own image; build it from infra/docker/renderer.Dockerfile) |
caddy:2.11.7-alpine@sha256:e70b164d… | renderer.enabled (the renderer's gateway; keep the digest when you mirror it, images.registry prefixes it) |
Generate the exact list for your values file with:
helm template critlab charts/critlab -f values.yaml | grep 'image:' | sort -uPoint the chart at your mirror: image.registry and image.repositoryPrefix retarget critlab's own images, images.registry prefixes every third-party image, and global.imagePullSecrets supplies the pull credential to every pod.
2. Turn off outbound calls
| Feature | Air-gapped setting |
|---|---|
| Page titles for links pasted into comments | LINK_PREVIEWS=off. Badges then show domain and path; links to your own instance still show the prototype name. |
SMTP_URL pointing at your internal relay. Never set RESEND_API_KEY. | |
| Voice transcription | Local whisper.cpp (below), or TRANSCRIBE_PROVIDER=none. Never openai. |
| Google / Microsoft sign-in | Leave the GOOGLE_* and MICROSOFT_* variables unset; use SAML against your internal IdP. |
| SAML metadata URL | Paste the IdP's metadata XML instead, or set SSO_ALLOW_PRIVATE_METADATA_HOSTS=1 if the IdP serves it on your network. |
| Integrations | Leave ISSUE_TRACKERS_ENABLED, LINEAR_* and JIRA_* unset. Slack-compatible webhooks to an internal Mattermost or Rocket.Chat need INTEGRATIONS_ALLOW_ANY_WEBHOOK_HOST=1. |
| Billing | BILLING_PROVIDER unset or none (the default). The app's CSP then never mentions the payment provider. |
| Error reporting | SENTRY_DSN unset (the default): nothing is initialised and nothing is sent. To collect reports inside the air gap, point it at an internal GlitchTip. See Monitoring. |
| Product analytics | ANALYTICS unset (the default on a self-hosted install, where BILLING_PROVIDER is none): nothing is recorded. If you turn it on it is first-party only: rows in your own Postgres, nothing sent anywhere. See Monitoring. |
| Framework telemetry | Already off: the images set NEXT_TELEMETRY_DISABLED=1, and the sign-in library's own usage telemetry is switched off in code. |
| External images and fonts in prototypes | Off by default per prototype; leave it off. |
The licence key is verified offline against a public key compiled into the build. See Licence.
3. Transcribe voice notes locally
The web-whisper image is the web image plus ffmpeg, a statically built whisper.cpp and a baked-in ggml-base.en model, checksum-verified at build time. It makes no network calls at runtime.
docker build -f infra/docker/web-whisper.Dockerfile -t registry.example.com/critlab/web-whisper:0.2.0 .web:
image:
variant: whisperThat switches the chart to TRANSCRIBE_PROVIDER=command with a matching TRANSCRIBE_COMMAND and TRANSCRIBE_CONVERT_COMMAND. For a larger model, rebuild with the WHISPER_MODEL and WHISPER_MODEL_SHA256 build arguments and set web.transcription.model to match. Without transcription, voice notes are still stored and playable, just without a transcript.
4. The sandbox certificate
A wildcard certificate is the hard part without public ACME. Issue *.s.example.com from your internal CA (see Domains and TLS) and make sure your users' browsers trust that CA. If no wildcard can be issued at all, path mode works with the limitation described there.
5. The CLI and agents
Install @critlab/cli from your internal npm mirror (or vendor it) rather than npx-ing it from the public registry, and point it at your instance with CRITLAB_API.