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.
Prerequisites
Section titled “Prerequisites”Cluster access
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
CREATE DATABASE). No extension is needed: vectors are kept in Qdrant through elitea-vector (vector.enabled), not in PostgreSQL.NATS JetStream
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
main.env.STORAGE_CONTAINER.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.Secrets to create beforehand
Section titled “Secrets to create beforehand”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.
Two settings with no chart default
Section titled “Two settings with no chart default”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:
| Setting | What it is |
|---|---|
llmGateway.env.GATEWAY_SELF_LLM_ORIGINS | Every 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.
Choose a values file
Section titled “Choose a values file”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).
The full capability set, matching docker-compose.standalone-full.yml — every capability flag on, the runtime plane on (agent execution, index ingest), sized for postgresql.maxConnections: 200 across up to main.autoscaling.maxReplicas: 4. Needs both the runtime material Secret and the auth material Secret. This is the reference production shape.
The cheapest install that can seed an AI provider credential through the product’s own API instead of hand-written SQL: production (Form) authentication plus ELITEA_CONFIGURATIONS_ENABLED, no runtime plane — no agent execution, no chat. Needs the auth material Secret, but no Redis, no runtime material Secret and no worker: the Form sign-in state is in PostgreSQL.
Google Kubernetes Engine with no static cloud credential: Cloud SQL with sslmode=require and a hand-prepared database, keyless GCS through Workload Identity, the GKE Ingress with its BackendConfig and FrontendConfig, Google’s load-balancer ranges in the NetworkPolicy, and every Secret from Secret Manager through externalSecrets. Sized for about 4 vCPU and 9 GiB of requests in steady state, with every default on. See Deploy on GKE with Argo CD.
A minimal single-replica shape (image tag shadow-v0.1, DATABASE_URL inlined for a throwaway environment, autoscaling and the pod disruption budget both off). Use it for a disposable smoke/shadow environment, not as a production starting point.
Install: one chart, one namespace
Section titled “Install: one chart, one namespace”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:
deploy/helm/elitea/build-deps.sh # helm dependency build, from Chart.lockhelm 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-namespaceThe 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.
The migration job and DB bootstrap
Section titled “The migration job and DB bootstrap”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.
First-admin bootstrap
Section titled “First-admin bootstrap”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.
Capability flags this install serves
Section titled “Capability flags this install serves”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.
| Flag | Default install | values-standalone.yaml | Needs |
|---|---|---|---|
ELITEA_ARTIFACTS_ENABLED | on | on | object storage configured |
ELITEA_CONFIGURATIONS_ENABLED | derived | on | any authentication and ELITEA_AI_PROJECT_ID |
ELITEA_PROJECT_INFO_ENABLED | off | on | production authentication |
ELITEA_CONFIGURATIONS_MUTATION_ENABLED | off | off | ELITEA_CONFIGURATIONS_ENABLED and runtime.enabled |
ELITEA_INDEX_TYPES_ENABLED | off | on | production authentication |
ELITEA_APPLICATION_SKILLS_ENABLED | off | on | production authentication |
ELITEA_RUNTIME_ENABLED (main.runtime.enabled) | on | on | production authentication and runtime material |
Post-install checks
Section titled “Post-install checks”Liveness through the edge
Terminal window curl -si https://elitea.example.com/healthz # 200 {"status":"ok"} with "vary: Origin"/readyzand/startupzare deliberately not routed by the edge: their bodies name each dependency’s state. The chart’s probes reach them on the pod directly.Admin console
/admin/app— sign in as the account you named above.Docs and API reference
/docs/(this site) and/api/docs(the OpenAPI UI) should both resolve oncemain.ingress(or a route of your own to elitea-main) publishes the platform: elitea-main relays/appand/docsto 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.