Kubernetes Deployment#
TriliumNext provides an official Helm chart built on the bjw-s common library (v3.3.2). The library handles Kubernetes primitives (deployments, services, ingress, probes); the Trilium chart adds a configini values block that maps directly to Trilium's config.ini file structure .
The container image is triliumnext/notes (DockerHub) / ghcr.io/triliumnext/trilium (GHCR) at internal port 8080 . For the full set of supported Kubernetes-level values.yaml keys, see the bjw-s reference values .
Helm Chart Configuration#
The chart's values.yaml exposes the following key settings :
| Key | Default | Notes |
|---|---|---|
controllers.main.containers.trilium.image.tag | v0.92.4 | Pin to a specific release |
persistence.data.type | persistentVolumeClaim | Mounts Trilium data directory |
configini.network.port | 8080 | Internal container port |
configini.network.trustedReverseProxy | true | Critical for Kubernetes — enables Express trust proxy |
configini.general.noAuthentication | false | Set true only on fully private networks |
Ingress: The ingress block is commented out by default. When enabled, add the annotation nginx.ingress.kubernetes.io/proxy-body-size: "0" to remove the body size limit on file uploads .
Environment variable overrides: All configini values can also be set via environment variables following the TRILIUM_[SECTION]_[KEY] pattern (e.g., TRILIUM_NETWORK_PORT, TRILIUM_NETWORK_TRUSTEDREVERSEPROXY). Environment variables take precedence over config.ini .
Health Check Endpoint#
Use GET /api/health-check for Kubernetes liveness and readiness probes. This endpoint:
- Requires no authentication
- Is not rate-limited
- Returns
{ "status": "ok" }with HTTP 200
# Example probe configuration
livenessProbe:
httpGet:
path: /api/health-check
port: 8080
readinessProbe:
httpGet:
path: /api/health-check
port: 8080
The bjw-s common library defaults probes to TCP socket checks, not HTTP path checks . If you've customized probes to use an HTTP path, /api/health-check is the correct target — not /login.
v0.104.0 regression: In this release, a rate limiter initialization bug (ERR_ERL_PERMISSIVE_TRUST_PROXY) caused HTTP probes pointed at /login to fail, triggering pod restart loops . Switching to /api/health-check avoids the issue entirely.
Rate Limiting and Trust Proxy#
Trilium rate-limits login endpoints at 10 requests per IP per 15 minutes using express-rate-limit . The limiter applies to POST /login, POST /api/login/sync, POST /api/login/token, and POST /api/sender/login.
Rate limiting uses the client IP as the key. Behind a reverse proxy, Express must be told to trust X-Forwarded-For headers; otherwise all requests appear to come from the proxy's IP and rate limiting becomes ineffective (or produces errors). This is configured via trustedReverseProxy in config.ini (or TRILIUM_NETWORK_TRUSTEDREVERSEPROXY env var), which maps to app.set("trust proxy", ...) at startup .
trustedReverseProxy value | Behavior |
|---|---|
false (default) | Ignores X-Forwarded-* headers |
true | Trusts leftmost IP in X-Forwarded-For |
IP/CIDR (e.g. 10.0.0.0/8) | Trusts only that proxy range |
Express shortcut (loopback, uniquelocal) | Trusts predefined IP ranges |
The Helm chart sets trustedReverseProxy: true by default , which is correct for most Kubernetes ingress setups but triggers a known v0.104.0 bug.
v0.104.0 ERR_ERL_PERMISSIVE_TRUST_PROXY regression: When trust proxy is set to true, express-rate-limit v7+ raises an error (ERR_ERL_PERMISSIVE_TRUST_PROXY) because it considers this configuration insecure (anyone can spoof their IP via X-Forwarded-For). This error surfaced in v0.104.0 and caused various request failures in proxied environments . A fix is included in nightly builds after 2026-07-18. As a workaround, set trustedReverseProxy to a specific IP/CIDR matching your ingress controller rather than true.
Session Management Behind Reverse Proxies#
Trilium uses an Express session stored in SQLite (cookie name trilium.sid; SameSite=lax; HttpOnly; default 21-day TTL). Session IDs are sent to the client via Set-Cookie and must round-trip intact on subsequent requests.
Session-affinity cookie conflict (v0.104.0 / Kubernetes): Kubernetes ingress controllers and load balancers often inject their own Set-Cookie headers (e.g., ws-server session-affinity cookies) for routing stickiness. When these additional cookies appear in responses to the desktop client, Trilium cannot match the session — resulting in 401 Logged in session not found errors and sync hangs . The root cause is described in issue #10548.
Workarounds:
- Remove or rename session-affinity cookies on the ingress/load balancer so they don't conflict with
trilium.sid. - Update to a nightly build after 2026-07-18, which includes a fix that makes the desktop client tolerate extra proxy-injected cookies .
WebSocket requirements: Trilium's real-time sync uses WebSockets. Your ingress must pass Upgrade and Connection: upgrade headers. For nginx ingress, add these to the proxy configuration :
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
OIDC + sync interaction: When OIDC authentication is enabled, the global OIDC middleware can interfere with the sync protocol's separate session management. If desktop sync hangs after OIDC login, see issue #10548 — the session-affinity cookie workaround above also resolves it .