Skip to content

Helm Setup

Install Zendrite using the devxy/helm-zendrite Helm chart.

Using OCI:

helm install zendrite oci://codefloe.com/devxy/zendrite

Or using a classic Helm repository:

helm repo add codefloe.com https://codefloe.com/api/packages/devxy/helm
helm install zendrite codefloe.com/zendrite

Create a values.yaml file and configure it to your liking. All possible values can be found in the chart README, but at least you need to configure a server_name:

zendrite_config:
  global:
    server_name: "localhost"

If you are going to use an existing Postgres database, you’ll also need to configure this connection:

zendrite_config:
  global:
    database:
      connection_string: "postgresql://PostgresUser:PostgresPassword@PostgresHostName/ZendriteDatabaseName"
      max_open_conns: 90
      max_idle_conns: 5
      conn_max_lifetime: -1

The chart can deploy the backend that Element Call needs: two LiveKit nodes and two lk-jwt-service replicas, sharing their state through an external Valkey or Redis. It then adds the backend to rtc_foci, enables delayed events for up to 24 hours, and serves LiveKit and lk-jwt-service on a host of their own through the Ingress or HTTPRoute the chart already creates.

matrixRTC:
  enabled: true
  hostName: rtc.example.com
  livekitAuth:
    # a Secret with an API secret of at least 32 characters
    existingSecret: livekit
  redis:
    # an HA Valkey, for example the valkey-io chart with Sentinel and HAProxy
    address: valkey-haproxy.valkey.svc:6379
    existingSecret: valkey

Each LiveKit pod runs with host networking on a node of its own, and clients send media straight to it on 7881/TCP and 7882/UDP, so open these ports on every node that may run LiveKit. A namespace that enforces the baseline Pod Security level rejects host networking and needs privileged. A stopping LiveKit pod serves its running calls until they end, for up to five hours, so a rollout waits for each node’s calls. The chart README lists all matrixRTC values.

The chart can run several candidates with one ready, safely active homeserver. HA is experimental; read High availability and HA storage and recovery before enabling it.

The values below require the chart release that adds HA support, tracked in devxy/helm-zendrite#35. Earlier releases have no ha or replicaCount values, no Lease RBAC and no status Service, and enabling HA against them leaves pods without permission to acquire their Lease.

replicaCount: 3

ha:
  enabled: true

# maxSurge must be 0: a surged candidate cannot become Ready while the old one
# still owns the homeserver, so the rollout would wait for it forever.
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 0
    maxUnavailable: 1

This restarts candidates one at a time and lets a standby take over, which is what you want for a configuration change or a same-version restart.

A version change is different. Candidates pin themselves to one executable fingerprint recorded on the Lease, so new-version pods cannot acquire it and the deployment ends with no owner at all. Use the maintenance window described in High availability for any version change.

replicaCount above 1 without ha.enabled is rejected, because running several active replicas without leader election corrupts shared state. A rolling update with a nonzero maxSurge is rejected as well: a surged candidate cannot be promoted while the old one still holds ownership, so the rollout would deadlock.

The chart creates a service account with get, update and create on the homeserver’s Lease, a headless status Service so standbys can be scraped, a disruption budget, and alert rules. It does not provision the HA prerequisites. Point the chart at an external HA PostgreSQL, an external replicated JetStream, and shared media, and disable the bundled PostgreSQL.

The chart comes with a dependency on Postgres, which can be installed alongside Zendrite. Enable it in your values.yaml:

postgresql:
  enabled: true # this installs Postgres
  primary:
    persistence:
      size: 1Gi # defines the size for $PGDATA

zendrite_config:
  global:
    server_name: "localhost"

Using this option, the database.connection_string will be set for you automatically.