> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nt3.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Kubernetes (Helm)

> Install Entri on Kubernetes with the Helm chart: external or bundled databases, secrets, ingress, migrations and scaling.

The Helm chart is published as an OCI artifact at `oci://ghcr.io/nt3-io/charts/entri`. Chart versions match Entri releases: chart `1.2.3` deploys image `ghcr.io/nt3-io/entri:1.2.3`. The source is in [`deploy/helm/entri`](https://github.com/nt3-io/platform/tree/main/deploy/helm/entri).

## Requirements

* Kubernetes 1.25+ and Helm 3.10+
* MongoDB 6+ running as a **replica set** (recommended; Atlas or a single-node replica set work). A standalone `mongod` works only with `MONGO_TRANSACTIONS=false`, which weakens the guarantees of auth writes.
* Redis 6+
* An ingress controller. The examples use [ingress-nginx](https://kubernetes.github.io/ingress-nginx/).

The chart can run MongoDB and Redis for you as single-pod StatefulSets. That suits small installs and evaluation. For production, use managed services or an operator.

> [!WARNING]
>
> **GitOps and `helm template` users** (Argo CD, Flux with client-side rendering, Kustomize, plain manifests): the chart reuses generated secrets through Helm's `lookup`, which returns nothing when the chart is rendered client-side. `BETTER_AUTH_SECRET`, `ENCRYPTION_KEY` and the bundled MongoDB/Redis passwords would then be **regenerated on every render**, logging everyone out, making encrypted data unreadable and locking Entri out of its own MongoDB. You **must** create these Secrets yourself and set `existingSecret`, plus `mongodb.auth.existingSecret` and `redis.auth.existingSecret` when the bundled databases are enabled. See [Plain manifests](#plain-manifests-helm-template).

## Quick install (bundled databases)

```bash theme={null}
helm install entri oci://ghcr.io/nt3-io/charts/entri \
  --version 1.2.3 \
  --namespace entri --create-namespace \
  --set publicUrl=https://entri.example.com \
  --set mongodb.enabled=true --set redis.enabled=true \
  --set ingress.enabled=true --set ingress.className=nginx \
  --set 'ingress.hosts[0].host=entri.example.com' \
  --set 'ingress.hosts[0].paths[0].path=/' \
  --set 'ingress.hosts[0].paths[0].pathType=Prefix'
```

Replace `1.2.3` with the [latest release](https://github.com/nt3-io/platform/releases). `publicUrl` sets `BETTER_AUTH_URL` and `APP_HOST`. It must be the URL users open, without a trailing slash.

Then open `https://entri.example.com` and sign up. The first account on an empty instance can always sign up. Afterwards, set `config.DISABLE_SIGNUP=true` so that only invited users can join.

> [!TIP]
>
> Keep your settings in a values file (`-f values.yaml`) rather than `--set` flags. You need the same file for every upgrade.

## Production install (external databases)

Create a Secret with the secret environment variables, then point the chart at it with `existingSecret`:

```bash theme={null}
kubectl create namespace entri
kubectl -n entri create secret generic entri-env \
  --from-literal=MONGO_URI='mongodb+srv://user:pass@cluster.example.net/entri' \
  --from-literal=REDIS_URI='redis://:pass@redis.example.net:6379' \
  --from-literal=BETTER_AUTH_SECRET="$(openssl rand -base64 48)" \
  --from-literal=ENCRYPTION_KEY="$(openssl rand -hex 32)"
```

```yaml values.yaml theme={null}
publicUrl: https://entri.example.com
existingSecret: entri-env

config:
  DISABLE_SIGNUP: "true"
  EMAIL_PROVIDER: smtp
  EMAIL_FROM: "Entri <noreply@example.com>"
  SMTP_HOST: smtp.example.com
  SMTP_PORT: "587"

ingress:
  enabled: true
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/proxy-buffering: "off"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    cert-manager.io/cluster-issuer: letsencrypt
  hosts:
    - host: entri.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: entri-tls
      hosts:
        - entri.example.com
```

```bash theme={null}
helm install entri oci://ghcr.io/nt3-io/charts/entri --version 1.2.3 \
  -n entri -f values.yaml
```

`MONGO_URI` must include the database name in its path (`/entri`). See [Database settings](https://docs.nt3.io/self-hosting/configuration.md#database).

## Configuration and secrets

The chart turns values into environment variables:

| Value | Rendered into | Use it for |
| - | - | - |
| `config.*` | ConfigMap | Non-secret settings, such as `DISABLE_SIGNUP`, `EMAIL_*`, `SEARCH_PROVIDER`, `TRUST_PROXY`. Any variable from the [configuration reference](https://docs.nt3.io/self-hosting/configuration.md) can go here. Empty values are skipped. |
| `secrets.*` | Secret | API keys, SMTP password, OAuth secrets, GitHub App credentials. Empty values are skipped. |
| `mongoUri`, `redisUri` | Secret | External database connection strings. Ignored when the bundled database is enabled. |
| `existingSecret` | — | Use your own Secret instead of `secrets`, `mongoUri` and `redisUri`. The chart then creates no Secret. |
| `extraEnv` | Container `env` | Anything else, for example values from another Secret via `valueFrom`. Applied to the app and the migration job. |

When you use `existingSecret`:

* The Secret must exist **before** `helm install`, because the migration hook reads `MONGO_URI` from it.
* Put every secret variable you need in it: `MONGO_URI`, `REDIS_URI` (unless the bundled databases are enabled), `BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`, plus API keys, `SMTP_PASS`, OAuth secrets and so on.
* With the bundled MongoDB or Redis, the chart sets `MONGO_URI` / `REDIS_URI` itself, overriding any value in your Secret.

### Generated secrets

Without `existingSecret`, the chart generates `BETTER_AUTH_SECRET` and `ENCRYPTION_KEY` on first install if you leave them empty. They are stored in the release Secret (named `entri` for a release called `entri`), reused on every upgrade, and the Secret carries `helm.sh/resource-policy: keep`, so it survives `helm uninstall`.

> [!WARNING]
>
> **Back up `ENCRYPTION_KEY`.** Without it, Entri cannot decrypt the AI keys organizations have stored.
>
> ```bash theme={null}
> kubectl -n entri get secret entri -o jsonpath='{.data.ENCRYPTION_KEY}' | base64 -d
> ```

The bundled MongoDB and Redis passwords are generated the same way, in the Secrets `entri-mongodb` (keys `mongodb-root-password`, `mongodb-keyfile`) and `entri-redis` (key `redis-password`), also kept on uninstall. To manage them yourself, create those Secrets and set `mongodb.auth.existingSecret` / `redis.auth.existingSecret`. Passwords must be URL-safe.

MongoDB applies the root password only when it first initializes its data volume. If you later pass a different `mongodb.auth.rootPassword`, the chart fails with an explanation instead of locking Entri out. Rotate it manually: change it inside MongoDB (`db.getSiblingDB("admin").changeUserPassword(...)`), update the `mongodb-root-password` key of the Secret, then upgrade. Changing the Redis password is safe: Redis and Entri restart together.

## Bundled databases

| Value | Default | Notes |
| - | - | - |
| `mongodb.enabled` | `false` | Single-node replica set (`rs0`) StatefulSet running `mongo:7`, with keyfile auth |
| `mongodb.database` | `entri` | |
| `mongodb.persistence.size` | `8Gi` | Plus `storageClass` |
| `redis.enabled` | `false` | `redis:7-alpine` StatefulSet with append-only persistence and password auth |
| `redis.persistence.size` | `1Gi` | Plus `storageClass` |

Back up the bundled MongoDB with `mongodump`:

```bash theme={null}
kubectl -n entri exec entri-mongodb-0 -- sh -c \
  'mongodump --quiet --archive --gzip --db entri \
     -u "$MONGO_INITDB_ROOT_USERNAME" -p "$MONGO_INITDB_ROOT_PASSWORD" \
     --authenticationDatabase admin' > entri-$(date +%F).archive.gz
```

Redis holds only short-lived caches, locks and progress messages, so it needs no backup.

## Ingress, WebSockets and SSE

Entri needs two things from the ingress:

* **WebSockets** for the progress channel at `/ws/progress`. ingress-nginx supports them out of the box.
* **Unbuffered Server-Sent Events** with long timeouts, for import, AI batch-translation and GitHub sync progress.

The annotations for ingress-nginx (also listed in `values.yaml`):

```yaml theme={null}
ingress:
  annotations:
    nginx.ingress.kubernetes.io/proxy-buffering: "off"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
```

For other controllers, configure the equivalent settings.

`TRUST_PROXY` is unset by default. Entri then trusts `X-Forwarded-For` from proxies on loopback and private networks, which covers in-cluster ingress controllers. Set `config.TRUST_PROXY` to a hop count or CIDR list only if a load balancer with a public IP forwards to Entri directly. See [Server settings](https://docs.nt3.io/self-hosting/configuration.md#server).

## Replicas and sticky sessions

Keep `replicaCount: 1`. AI batch-translation progress streams live in process memory, so the request that opens a stream must reach the pod that runs the job. The autoscaler (`autoscaling.enabled`) is off by default for the same reason.

If you run more than one replica, enable cookie affinity on the ingress:

```yaml theme={null}
replicaCount: 2
ingress:
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "entri-affinity"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"
```

Import progress over WebSockets is shared between pods through Redis pub/sub and does not need affinity.

## Network policies

With `networkPolicy.enabled: true` (default), the chart adds NetworkPolicies for the **bundled** MongoDB and Redis: only Entri pods and the migration job can connect to them. Nothing is rendered for external databases.

The Entri pods themselves are not restricted, because ingress controllers differ. To lock them down, add your own policy that selects `app.kubernetes.io/name: entri` and allows traffic from your ingress controller's namespace on the `http` port (3333).

## Database migrations

With `migrations.enabled: true` (default), every `helm install` and `helm upgrade` runs a Job named `entri-migrate` that executes `migrate` with the new image:

* **External MongoDB**: the Job is a `pre-install,pre-upgrade` hook. It runs before the new pods start. If it fails, the upgrade fails and the old pods keep running.
* **Bundled MongoDB**: on first install the Job is a `post-install` hook, because the database does not exist before install. On upgrades it is a `pre-upgrade` hook.

The Job retries up to `migrations.backoffLimit` (10) times, which covers a database that is still starting, with a deadline of `migrations.activeDeadlineSeconds` (900). Successful Jobs are deleted. A failed Job is kept, so you can read its log:

```bash theme={null}
kubectl -n entri logs job/entri-migrate
```

## Security defaults

* Pods run as non-root (UID 1000) with a read-only root filesystem, all capabilities dropped and the `RuntimeDefault` seccomp profile. `/tmp` is an `emptyDir`.
* Probes: startup and liveness on `/api/v1/health/liveness`, readiness on `/api/v1/health/readiness` (checks MongoDB and Redis).
* Default resources: requests of 250m CPU and 512Mi memory, with a 1Gi memory limit.

The full list of values is in the [chart README](https://github.com/nt3-io/platform/blob/main/deploy/helm/entri/README.md) and `values.yaml`.

## Plain manifests (helm template)

If you deploy with Kustomize, Argo CD without Helm, or `kubectl apply`, render the chart once and commit the output:

```bash theme={null}
kubectl create namespace entri
kubectl -n entri create secret generic entri-env \
  --from-literal=MONGO_URI='mongodb+srv://user:pass@cluster.example.net/entri' \
  --from-literal=REDIS_URI='redis://:pass@redis.example.net:6379' \
  --from-literal=BETTER_AUTH_SECRET="$(openssl rand -base64 48)" \
  --from-literal=ENCRYPTION_KEY="$(openssl rand -hex 32)"

# Render the migration Job and the rest of the app separately
helm template entri oci://ghcr.io/nt3-io/charts/entri --version 1.2.3 \
  --namespace entri -f values.yaml --set existingSecret=entri-env \
  --show-only templates/migration-job.yaml > entri-migrate.yaml
helm template entri oci://ghcr.io/nt3-io/charts/entri --version 1.2.3 \
  --namespace entri -f values.yaml --set existingSecret=entri-env \
  --no-hooks > entri.yaml

# 1. Run migrations first and wait for them
kubectl -n entri delete job entri-migrate --ignore-not-found
kubectl -n entri apply -f entri-migrate.yaml
kubectl -n entri wait --for=condition=complete job/entri-migrate --timeout=15m

# 2. Then roll out the app
kubectl -n entri apply -f entri.yaml
```

Things to know:

* **Always use `existingSecret`.** `helm template` cannot read the cluster, so it writes secret values, or newly generated random ones, into the YAML, and they change on every render.
* **Bundled databases need their own Secrets.** For the same reason, set `mongodb.auth.existingSecret` and `redis.auth.existingSecret` to Secrets you created yourself. Otherwise each render generates a new MongoDB password, and MongoDB, which keeps the password it was first initialized with, starts rejecting Entri.
  ```bash theme={null}
  kubectl -n entri create secret generic entri-mongodb-auth \
    --from-literal=mongodb-root-password="$(openssl rand -hex 24)" \
    --from-literal=mongodb-keyfile="$(openssl rand -base64 756 | tr -d '\n')"
  kubectl -n entri create secret generic entri-redis-auth \
    --from-literal=redis-password="$(openssl rand -hex 24)"
  ```
* **Migrations.** `kubectl apply` ignores Helm hooks, so run the migration Job yourself, as above: on every upgrade, delete the old Job, apply the new one, and wait for it to complete **before** applying the new Deployment. If the Job fails (`kubectl -n entri logs job/entri-migrate`), do not apply `entri.yaml`; the old version keeps running.
* **Bundled MongoDB, first install only:** the database must exist before the Job can run, so apply `entri.yaml` first (it contains the MongoDB StatefulSet), then the Job. On upgrades, always run the Job first.

## Upgrading

```bash theme={null}
helm upgrade entri oci://ghcr.io/nt3-io/charts/entri --version 1.3.0 -n entri -f values.yaml
```

The migration hook runs before the new pods start. See [Upgrading](https://docs.nt3.io/self-hosting/upgrading.md) for backups and rollback.

## Uninstalling

```bash theme={null}
helm uninstall entri -n entri
```

By design, some resources stay behind:

* The Secrets `entri`, `entri-mongodb` and `entri-redis` (`helm.sh/resource-policy: keep`). They hold `ENCRYPTION_KEY` and the database passwords that match the data on the volumes.
* The PersistentVolumeClaims of the bundled databases (`data-entri-mongodb-0`, `data-entri-redis-0`). Kubernetes never deletes StatefulSet volumes automatically.

If you install again with the same release name and namespace, the chart picks up the kept Secrets and volumes, and your data is still there. To remove everything permanently:

```bash theme={null}
kubectl -n entri delete secret entri entri-mongodb entri-redis
kubectl -n entri delete pvc data-entri-mongodb-0 data-entri-redis-0
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.