# Requirements and sizing

> What you need before installing: hostnames, a wildcard certificate, Postgres, Redis, S3-compatible storage, and how much hardware.

Source: https://critlab.app/docs/self-hosting/requirements

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](/docs/self-hosting/domains-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](/docs/self-hosting/helm#migrations).
- **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](/docs/self-hosting/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](/docs/self-hosting/evaluation).
- **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](#the-renderer-optional).
- **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.

| 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_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](/docs/self-hosting/helm#the-renderer-optional-off-by-default). The Compose file ships with the repository, and its `.env.example` lists the switches.

> **Open questions**
>
> - **Published images.** The CI workflow builds all images on each release tag, but pushing to a registry is not switched on yet. Until it is, build the images yourself (see [Production install with Helm](/docs/self-hosting/helm)).
> - **The renderer is young.** It has run under Docker on `linux/arm64` and, once, on `linux/amd64`. The Helm component renders and validates but has never run on a cluster, and its memory use under load is unmeasured. See [The renderer](#the-renderer-optional).
> - **Measured capacity.** There is no load test yet that turns team size into a node size; the estimates above come from the chart's defaults.
