Skip to content

Install with Helm

This walks a Kubernetes install of the elitea chart end to end. It is one chart for everything: the platform, NATS JetStream with its stream bootstrap, the vector store and, where a cluster needs it, the sandbox-runtime installer. Read Production deployment overview first for the topology and what the chart does not provision. For a first install on an empty cluster, Deploy from scratch has every Secret, certificate authority, database step and edge rule in one place, with a complete values file.

Cluster access

A Kubernetes cluster with kubectl and helm configured against it. Helm 4.3 or newer is recommended; Helm 3.8 or newer also renders the chart, which is what Argo CD uses. With Helm 4, --atomic is --rollback-on-failure.

PostgreSQL

Reachable, with an administrator DSN available for the database-preparation Job (CREATE DATABASE). No extension is needed: vectors are kept in Qdrant through elitea-vector (vector.enabled), not in PostgreSQL.

NATS JetStream

Bundled: the chart runs it (nats.enabled, on by default) and creates its streams with a hook Job (natsBootstrap). The platform needs no Redis: the runtime command bus, live updates and presence are on NATS, and the chart refuses values that still set runtimeRedis.

Object storage

An S3-compatible bucket, created ahead of the first sync and named by main.env.STORAGE_CONTAINER.

cert-manager

Required. The bundled NATS CA, the NATS client identities and the LLM gateway’s mTLS hop (on by default) are all cert-manager Certificates. The mTLS issuers default to ClusterIssuers elitea-internal-ca and elitea-runtime-ca (certManager.issuers), yours to create; or set certManager.issuers.create: true and the chart makes them, namespaced, over your runtime CA Secret.

Nothing secret goes in a values file — a Helm parameter renders into a ConfigMap, readable by anyone with get on the namespace. Create these Kubernetes Secrets first; the chart only ever takes their names.

SECRETS_MASTER_KEY

Wraps each project’s vault key. Required by both elitea-main and the gateway, and it must be the same key: elitea-main reads the gateway’s reference (llmGateway.secrets.SECRETS_MASTER_KEY, default Secret elitea-llm-gateway-secrets, key secrets-master-key) unless you override both. Generate: python3 -c 'import base64, os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())'

postgresql.existingSecret

The database DSN, one Secret + key pair that every component reading the database (elitea-main, its migration Job, the scheduler, the LLM gateway, the workload-session Job) derives its DATABASE_URL from. Default names: Secret elitea-main-db, key database-url.

runtime.material.secretName (runtime plane only)

Agent-execution signing keys and listener certificates (from deploy/scripts/gen-runtime-certs.sh). Required if main.runtime.enabled: true.

fileConfig.authConfig.material.secretName (production auth)

Three files: the browser-attempt key, the PAT signing key and the Form users JSON. Required if using Form or mixed authentication. The auth document is elitea.auth.form.v2: it has no redis block, because the Form sign-in state is in PostgreSQL, and the attempt key is credentials.attempt_key_file. The Form users JSON is a sign-in path only when main.env.ELITEA_FORM_LOGIN_ENABLED is "true"; Form sign-in is off by default.

Gateway identity secret (GATEWAY_IDENTITY_SECRET)

Required on both main and gateway; must be identical. HMAC key for signing/verifying identity headers.

The chart refuses to render while either of these is empty — there is no safe guess for either, and a guessed value would look armed while guarding nothing:

SettingWhat it is
llmGateway.env.GATEWAY_SELF_LLM_ORIGINSEvery origin that reaches this deployment’s /llm surface, public and in-cluster, comma-separated — e.g. https://elitea.example.com/llm,http://elitea-main:8080/llm. global.publicUrl fills exactly that when this is empty.
llmGateway.egressPosture"allowlist" (only hosts in GATEWAY_EGRESS_ALLOWLIST / governance rows) or "public-unrestricted" (any public host, until the first governance row narrows it). Private-network reachability follows only from an entry that explicitly names a private address or block — never from the posture alone.

One value fills most of the URL-shaped settings: global.publicUrl (for example https://elitea.example.com) fills DEPLOYMENT_URL, the OIDC redirect URI, the authentication document’s public_origin, the gateway’s self origins, the worker’s base URL and SPIFFE ID and the Ingress host, where each is empty. global.trustedProxyCidrs fills both trusted-proxy lists. See Configuration. The chart also refuses a value that still holds a <...> placeholder of an example file, and lists every such path.

Also required: platform.aiProjectId, the id of the platform’s public/shared project — it must name a project schema (p_<id>) that already exists. It fills main.env.ELITEA_AI_PROJECT_ID, llmGateway.env.ELITEA_AI_PROJECT_ID and web.env.VITE_PUBLIC_PROJECT_ID at once; helm template refuses a values file where the three copies disagree.

The chart’s own defaults. The runtime plane, the worker, elitea-vector with the bundled Qdrant, DeepWiki and Inventory are on, one switch each (main.runtime.enabled, worker.enabled, vector.enabled with qdrant.enabled, deepwiki.enabled, inventory.enabled); set one to false explicitly to leave it out. The base file alone does not render: the values only you know (the authentication document and its public_origin, the public project, DEPLOYMENT_URL, the worker’s identity, the git allowlists, the network-policy peers, the gateway’s self origins and egress posture) are listed together in one refusal, so a first helm template is the checklist. Qdrant needs a PersistentVolumeClaim (qdrant.persistence, 50Gi on the default StorageClass).

One release installs everything, in one namespace. NATS requires TLS and a client certificate from every client (mTLS, no anonymous or password access), and those certificates come from the chart’s own NATS CA, a namespaced cert-manager Issuer, so NATS, its bootstrap and the platform share the release’s namespace by construction. cert-manager must be installed first.

From a clone, build the chart’s dependencies first. The upstream NATS chart and the sandbox-runtime installer are pinned in the committed Chart.lock, and charts/ is never committed:

Terminal window
deploy/helm/elitea/build-deps.sh # helm dependency build, from Chart.lock
helm install elitea deploy/helm/elitea \
-f deploy/helm/elitea/values-standalone.yaml \
--set postgresql.existingSecret=elitea-main-db \
--set-string llmGateway.env.GATEWAY_SELF_LLM_ORIGINS=https://elitea.example.com/llm/v1 \
--set-string llmGateway.egressPosture=public-unrestricted \
--set-string platform.aiProjectId=1 \
-n elitea --create-namespace

The published chart is the recommended source: oci://ghcr.io/elitea-ng/charts/elitea --version <version> in place of the path. Its package carries the same values files (helm pull --untar to read them) and both dependencies, so it needs no helm dependency build. In an air-gapped network, mirror that OCI chart; do not build from a clone.

The chart issues each NATS client (elitea-main, the LLM gateway, the scheduler, the worker) its own certificate and renders tls://elitea-nats.elitea.svc.cluster.local:4222 for it — no NATS credential goes in any value. The stream and KV bootstrap is a post-install,post-upgrade hook Job; until it has run, the workloads restart, then settle.

For three NATS nodes, add -f values-nats-ha.yaml (it raises the bootstrap’s replica count too). To use a NATS you run yourself, set nats.enabled=false and point natsClient.* at it. Under ArgoCD the same chart orders itself with sync waves — see GitOps with ArgoCD.

elitea-migrate runs as a pre-install hook; the database may start empty. The dbInit Job (hook weight -20) handles CREATE DATABASE for the agent-state database before migrations run. It creates no extension.

A fresh database seeds exactly one administrator, dev@elitea.ai, with no password on a single-sign-on deployment — name your own account instead through identity.initial_global_admins (a YAML list, wins when the authentication document has one) or main.env.ELITEA_INITIAL_GLOBAL_ADMINS (comma-separated, the usual path for a single-sign-on-only install with no document). With Form sign-in off (the default), the first OIDC or SAML sign-in that matches an entry — oidc:<sub>, saml:<nameid>, or email:<address> for an OIDC login with a verified address — is your first administrator. Write Form logins there only if you enable Form sign-in.

An entry is oidc:<sub>, saml:<nameid>, or email:<verified address> (matches only when the id_token asserts email_verified: true). The grant is one-shot: an account that already holds any administration role is left as is; one with none receives the grant on its next sign-in.

For OIDC with a verified e-mail address, use email:<your address>. You know this value before the first sign-in, so you can set it at install time.

For other cases, sign in once and open Settings › Profile. Each Sign-in reference row shows one exact value that the list accepts, for example oidc:<sub> or saml:<nameid>. Click a row to copy it. Add the value to the list, upgrade the release, and sign in again. You do not need to query the database. See Install Elitea for the browser-side walkthrough of this same step.

How to read this table

cmd/elitea-main gates whole route groups on these variables. values.yaml sets every one; values-standalone.yaml is the reference production set. ELITEA_CONFIGURATIONS_ENABLED ships empty in the base chart — _helpers.tpl derives "true" automatically once any authentication is configured and ELITEA_AI_PROJECT_ID is set; a stated value always overrides the derivation.

FlagDefault installvalues-standalone.yamlNeeds
ELITEA_ARTIFACTS_ENABLEDononobject storage configured
ELITEA_CONFIGURATIONS_ENABLEDderivedonany authentication and ELITEA_AI_PROJECT_ID
ELITEA_PROJECT_INFO_ENABLEDoffonproduction authentication
ELITEA_CONFIGURATIONS_MUTATION_ENABLEDoffoffELITEA_CONFIGURATIONS_ENABLED and runtime.enabled
ELITEA_INDEX_TYPES_ENABLEDoffonproduction authentication
ELITEA_APPLICATION_SKILLS_ENABLEDoffonproduction authentication
ELITEA_RUNTIME_ENABLED (main.runtime.enabled)ononproduction authentication and runtime material
  1. Liveness through the edge

    Terminal window
    curl -si https://elitea.example.com/healthz # 200 {"status":"ok"} with "vary: Origin"

    /readyz and /startupz are deliberately not routed by the edge: their bodies name each dependency’s state. The chart’s probes reach them on the pod directly.

  2. Admin console

    /admin/app — sign in as the account you named above.

  3. Docs and API reference

    /docs/ (this site) and /api/docs (the OpenAPI UI) should both resolve once main.ingress (or a route of your own to elitea-main) publishes the platform: elitea-main relays /app and /docs to elitea-web itself.

See Troubleshooting for what each endpoint answers and the application-level Health tab it complements.

What a Kubernetes install does NOT give you

Section titled “What a Kubernetes install does NOT give you”
No PostgreSQL

The chart provisions no database. The migration hook fails against a cluster where PostgreSQL does not already exist or where the Secret/host settings have not been pointed at it. NATS, by contrast, is part of the release: elitea-main’s live updates — the project SSE stream and canvas presence — need it with JetStream (ELITEA_EVENTS_NATS_URL, derived from the natsClient block), and so does the runtime plane: its command bus is JetStream in the RUNTIME account (ELITEA_RUNTIME_NATS_URL, as the elitea-main-runtime identity), consumed by the workers from their own WORKER account. The chart refuses main.runtime.enabled with no NATS to publish to.

No edge by default

No Ingress or HTTPRoute renders until main.ingress.enabled: true. Then it is one / rule to elitea-main, which is the whole edge: elitea-main redirects / to /app/, relays /app and /docs to elitea-web, and serves its MCP endpoint under /app itself. On GKE, main.ingress.gke adds the BackendConfig and FrontendConfig the Google load balancer needs.

Bring your own NATS

The bundled server lives in the release’s namespace, signed by the chart’s namespaced elitea-nats-ca Issuer. To use a NATS you run elsewhere, set nats.enabled: false, back a cert-manager ClusterIssuer with a CA used for NATS only, and point natsClient.service, natsClient.namespace and natsClient.tls.issuerRef at it; the bootstrap’s identities follow natsClient (natsBootstrap.tls.certificate.issuerRef). The bootstrap Job still runs against your server unless natsBootstrap.enabled: false. Your server must run JetStream and map the same client identities, or elitea-main refuses to start. deploy/helm/elitea/NATS.md has the contract.

Who can mint a NATS identity

Anyone who may create a cert-manager Certificate in the NATS namespace, or read its Secrets (the CA key is one), can obtain any NATS client identity. Restrict both with RBAC; the optional approver-policy (nats.security.approverPolicy) limits what the NATS issuers sign. See deploy/helm/elitea/NATS.md.

No secrets

Every sensitive value is a Kubernetes Secret — see “Secrets to create beforehand” above. In production, turn on externalSecrets and the chart renders an ExternalSecret for each one from your secret manager’s store; Deploy on GKE with Argo CD shows it with Google Secret Manager.

Continue to GitOps with ArgoCD to run this same install declaratively, or Configuration for the operator-facing settings after the pods are Ready. To run desktop threads sent to the cloud, add Cloud executor on Kubernetes.