Skip to content

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:

bash
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.tar
bash
docker load -i critlab-images-0.2.0.tar
docker push registry.internal.example/critlab/web:0.2.0

The full list, depending on which dependencies you bundle:

ImageNeeded when
critlab/web (or critlab/web-whisper)always
critlab/sandbox, critlab/realtime, critlab/migratealways
postgres:16-alpinepostgresql.enabled
redis:7-alpineredis.enabled
chrislusf/seaweedfs:4.47seaweedfs.enabled
amazon/aws-cli:2.32.17seaweedfs.enabled (creates the bucket once)
quay.io/oauth2-proxy/oauth2-proxy:v7.15.4iap.enabled
critlab/rendererrenderer.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:

bash
helm template critlab charts/critlab -f values.yaml | grep 'image:' | sort -u

Point 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

FeatureAir-gapped setting
Page titles for links pasted into commentsLINK_PREVIEWS=off. Badges then show domain and path; links to your own instance still show the prototype name.
EmailSMTP_URL pointing at your internal relay. Never set RESEND_API_KEY.
Voice transcriptionLocal whisper.cpp (below), or TRANSCRIBE_PROVIDER=none. Never openai.
Google / Microsoft sign-inLeave the GOOGLE_* and MICROSOFT_* variables unset; use SAML against your internal IdP.
SAML metadata URLPaste the IdP's metadata XML instead, or set SSO_ALLOW_PRIVATE_METADATA_HOSTS=1 if the IdP serves it on your network.
IntegrationsLeave ISSUE_TRACKERS_ENABLED, LINEAR_* and JIRA_* unset. Slack-compatible webhooks to an internal Mattermost or Rocket.Chat need INTEGRATIONS_ALLOW_ANY_WEBHOOK_HOST=1.
BillingBILLING_PROVIDER unset or none (the default). The app's CSP then never mentions the payment provider.
Error reportingSENTRY_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 analyticsANALYTICS 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 telemetryAlready 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 prototypesOff 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.

bash
docker build -f infra/docker/web-whisper.Dockerfile -t registry.example.com/critlab/web-whisper:0.2.0 .
yaml
web:
  image:
    variant: whisper

That 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.