Skip to content

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.com pointing 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 renderer profile of infra/compose/production.yml or Helm's renderer.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 set SENTRY_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.

ComponentReplicasCPU request / limitMemory request / limitStorage
web2200m / 1256Mi / 512Mi—
sandbox2100m / 500m128Mi / 256Mi—
realtime1100m / 500m128Mi / 256Mi—
Postgres (bundled)1250m / 1256Mi / 1Gi10Gi
Redis (bundled)1100m / 500m64Mi / 256Minone by default
SeaweedFS (bundled)1250m / 1256Mi / 1Gi20Gi
migrate (job, per install/upgrade)1100m / 500m128Mi / 256Mi—
renderer (optional)exactly 1500m / none2Gi / 2Gi—
renderer gateway (optional)150m / 500m32Mi / 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:

ItemDefault limit
Package, Free plan25 MiB
Package, paid plans100 MiB
Screenshot, per image (two per new thread)4 MiB
Voice note10 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

ImageSize (uncompressed)Built from
webabout 354 MBinfra/docker/web.Dockerfile
sandboxabout 275 MBinfra/docker/sandbox.Dockerfile
realtimeabout 232 MBinfra/docker/realtime.Dockerfile
migratesmall; runs once per install/upgradeinfra/docker/migrate.Dockerfile
web-whisper (optional)larger: adds ffmpeg, whisper.cpp and a modelinfra/docker/web-whisper.Dockerfile
renderer (optional)about 1.07 GBinfra/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_namespaces must 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 renderer profile of infra/compose/production.yml) or the Helm chart's renderer.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=off and CANVAS_CRAWL=off. The production Compose file defaults to both while its renderer profile is off; the Helm chart sets HEADLESS_RENDERER=off itself while renderer.enabled is false and leaves CANVAS_CRAWL to you (CANVAS_CRAWL=off also 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.