Self-hosting
Requirements and sizing
What you need before installing: hostnames, a wildcard certificate, Postgres, Redis, S3-compatible storage, and how much hardware.
Check this list before you install. Most failed installs trace back to the second item: the sandbox zone and its wildcard certificate.
Checklist
- An app hostname, e.g.
critlab.example.com. - A sandbox DNS zone you control, e.g.
s.example.com, with a wildcard record*.s.example.compointing at your ingress, and a wildcard TLS certificate for it (issued over DNS-01, or by an internal CA). It must not be the app hostname. If you cannot get a wildcard certificate, read about path mode in Domains and TLS first: it works, with a real limitation. - Postgres 16 (bundled by the chart, or managed). With a managed one, give critlab its own role that owns its database and is not a superuser; the migrations need nothing more. See Helm.
- Redis 7 (bundled by the chart, or managed).
- An S3-compatible bucket: SeaweedFS (bundled), AWS S3, Cloudflare R2, Ceph RGW or MinIO.
- An SMTP relay that can send from your domain. Sign-in is by emailed code, so without mail nobody can sign in except by reading codes from the server log. See Email.
- For production: Kubernetes 1.27 or later, Helm 3 and an ingress controller that can serve several hostnames with separate certificates. The chart has presets for ingress-nginx, Traefik and HAProxy Ingress.
- For evaluation: one machine with Docker (or Colima) and Node.js 22.23.4 or newer. See Evaluate with Docker Compose.
- Optional, for thumbnails and the HTML canvas: the renderer. A host (or, on Kubernetes, every node that can run it) whose kernel allows unprivileged user namespaces and whose AppArmor or SELinux policy does not refuse them to the container, 2 GiB of memory for the renderer itself, and either the
rendererprofile ofinfra/compose/production.ymlor Helm'srenderer.enabled. Details: The renderer. - Optional: a SAML or OIDC identity provider, a licence key for Team and Enterprise features, and a whisper.cpp model for local voice transcription.
Network
- Inbound: HTTPS on the app hostname and on the sandbox zone. WebSockets on
/ws(or a separate realtime hostname) with long read timeouts; the chart sets one hour for ingress-nginx. - Upload size: the ingress must accept request bodies of about 110 MiB (the default package limit of 100 MiB plus 5% for the multipart envelope). The chart sets this for ingress-nginx, Traefik and HAProxy Ingress.
- Outbound: nothing is required. Every outbound call is opt-in — your SMTP relay, an identity provider's metadata URL, integrations, and page titles for pasted links (on by default; turn off with
LINK_PREVIEWS=off), and error reporting (off unless you setSENTRY_DSN). critlab never phones home, including for licence checks.
Sizing
The numbers below are the Helm chart's default resource requests and limits, which is what critlab has been run with. They are a starting point, not a benchmark.
| Component | Replicas | CPU request / limit | Memory request / limit | Storage |
|---|---|---|---|---|
| web | 2 | 200m / 1 | 256Mi / 512Mi | — |
| sandbox | 2 | 100m / 500m | 128Mi / 256Mi | — |
| realtime | 1 | 100m / 500m | 128Mi / 256Mi | — |
| Postgres (bundled) | 1 | 250m / 1 | 256Mi / 1Gi | 10Gi |
| Redis (bundled) | 1 | 100m / 500m | 64Mi / 256Mi | none by default |
| SeaweedFS (bundled) | 1 | 250m / 1 | 256Mi / 1Gi | 20Gi |
| migrate (job, per install/upgrade) | 1 | 100m / 500m | 128Mi / 256Mi | — |
| renderer (optional) | exactly 1 | 500m / none | 2Gi / 2Gi | — |
| renderer gateway (optional) | 1 | 50m / 500m | 32Mi / 128Mi | — |
Summed, the defaults (renderer off) request about 1.3 CPU and 1.5 GiB of memory and are capped at 6 CPU and 4 GiB. The renderer adds 2 GiB of memory and half a CPU. Estimate: a cluster with 4 vCPU and 8 GiB free is comfortable for a team of a few dozen; 2 vCPU and 4 GiB is the practical floor. Raise web first as usage grows (the chart has an optional autoscaler for web and sandbox), then Postgres.
Storage estimates
Object storage dominates. Package files are stored once per content hash, so a new version that changes a few files adds only those files. Per item, the defaults allow:
| Item | Default limit |
|---|---|
| Package, Free plan | 25 MiB |
| Package, paid plans | 100 MiB |
| Screenshot, per image (two per new thread) | 4 MiB |
| Voice note | 10 MiB |
Estimate: plan on the total size of the builds your team uploads, plus a few MiB per comment thread. Postgres stays small by comparison; the chart's 10Gi default leaves years of headroom for most teams.
Images
| Image | Size (uncompressed) | Built from |
|---|---|---|
| web | about 354 MB | infra/docker/web.Dockerfile |
| sandbox | about 275 MB | infra/docker/sandbox.Dockerfile |
| realtime | about 232 MB | infra/docker/realtime.Dockerfile |
| migrate | small; runs once per install/upgrade | infra/docker/migrate.Dockerfile |
| web-whisper (optional) | larger: adds ffmpeg, whisper.cpp and a model | infra/docker/web-whisper.Dockerfile |
| renderer (optional) | about 1.07 GB | infra/docker/renderer.Dockerfile |
All images run as a non-root user on Node.js 22.23.4 (Alpine), the first 22.x release with the October 2026 security fixes, for linux/amd64 and linux/arm64. The renderer image is the exception: it is Debian 12 slim (Chrome needs glibc), and it has been run on linux/arm64 and, once, on linux/amd64.
The renderer (optional)
Dashboard thumbnails, the HTML canvas and AI review runs are rendered in a headless Chrome. The standard images contain no browser. critlab refuses a Chrome that runs without its own sandbox or is older than the minimum version it names, and renders nothing with it. A separate renderer container provides a Chrome that passes; it holds no app secret and reaches nothing but its own gateway.
- Memory: 2 GiB for the renderer, one browser at a time. Sizing beyond that has not been measured.
- Kernel and security modules: Chrome's sandbox needs unprivileged user namespaces.
user.max_user_namespacesmust be above 0, and an AppArmor or SELinux policy must not refuse them to the container. The renderer runs with no capabilities under a vendored seccomp profile; it never runs privileged or with the sandbox off. On Kubernetes that profile has to be placed on every node that can run the pod. - Runtime: Docker Compose (the
rendererprofile ofinfra/compose/production.yml) or the Helm chart'srenderer.enabled. Both are off by default. Kubernetes also needs a CNI that enforces NetworkPolicy. - Without it: cards show their placeholder, an HTML canvas waits, and an AI review run fails with the reason. Uploads are never blocked, and nothing is recorded against a version, so everything renders once a renderer is available. Say so up front with
HEADLESS_RENDERER=offandCANVAS_CRAWL=off. The production Compose file defaults to both while itsrendererprofile is off; the Helm chart setsHEADLESS_RENDERER=offitself whilerenderer.enabledis false and leavesCANVAS_CRAWLto you (CANVAS_CRAWL=offalso hides an HTML canvas that was already built).
The steps are in Production install with Helm. The Compose file ships with the repository, and its .env.example lists the switches.