Kubernetes (Helm)
Elara ships an official Helm chart. Storage is backed by an embedded bbolt database (single instance); raft-based HA and additional storage backends (PostgreSQL, S3) are on the roadmap.
Chart source: https://github.com/sergeyslonimsky/elara/tree/master/helm
TL;DR
helm repo add elara https://sergeyslonimsky.github.io/elara
helm install elara elara/elara
Or install directly from a checkout:
helm install elara ./helm/elara
Prerequisites
| Requirement | Notes |
|---|---|
| Kubernetes ≥ 1.25 | Enforced by Chart.yaml.kubeVersion |
| Helm ≥ 3.10 | For values.schema.json validation |
| A default StorageClass | bbolt persistence uses volumeClaimTemplates with RWO |
| (optional) Prometheus Operator | Required only when enabling ServiceMonitor |
| (optional) cert-manager | For automatic TLS on the Ingress |
Quick install examples
Default (minimum viable)
helm install elara ./helm/elara
Creates a single-replica StatefulSet with a 2 Gi RWO PVC, exposes an internal ClusterIP service on ports 8080 (HTTP/UI/ConnectRPC) and 2379 (etcd gRPC). No Ingress, no metrics, no tracing.
With HTTP ingress and TLS
helm install elara ./helm/elara \
--set ingress.enabled=true \
--set ingress.className=nginx \
--set ingress.hosts[0].host=elara.example.com \
--set ingress.tls[0].hosts[0]=elara.example.com \
--set ingress.tls[0].secretName=elara-tls
The Ingress exposes the HTTP/ConnectRPC port only. The etcd-compatible gRPC port (2379) is intentionally not routed through a standard HTTP Ingress — see gRPC exposure below.
With Prometheus ServiceMonitor
helm install elara ./helm/elara \
--set metrics.enabled=true \
--set metrics.serviceMonitor.enabled=true \
--set metrics.serviceMonitor.labels.release=kube-prometheus-stack
With OTLP tracing
helm install elara ./helm/elara \
--set tracing.enabled=true \
--set tracing.otlpEndpoint=http://otel-collector.observability:4318
With basic-auth (username + password)
helm install elara ./helm/elara \
--set config.ui.auth.enabled=true \
--set config.ui.auth.type=basic-auth \
--set config.ui.auth.basicAuth.username=admin@example.com \
--set config.ui.auth.basicAuth.password=ChangeMe123
The chart creates a {release}-auth Secret holding the sensitive values. The
admin is forced to change their password on first login — see
Basic Auth Setup.
For production, supply credentials from a pre-existing Secret (e.g. managed by external-secrets or Vault) and omit the plaintext values:
helm install elara ./helm/elara \
--set config.ui.auth.enabled=true \
--set config.ui.auth.type=basic-auth \
--set config.ui.auth.basicAuth.username=admin@example.com \
--set config.ui.auth.existingSecret=my-elara-auth-secret
The existing Secret must contain UI_AUTH_BASICAUTH_PASSWORD (or
UI_AUTH_OIDC_CLIENTSECRET when type=oidc) — it is mounted with envFrom,
so any other key in it also reaches the service as an env var.
With OIDC
helm install elara ./helm/elara \
--set config.ui.auth.enabled=true \
--set config.ui.auth.type=oidc \
--set config.ui.auth.oidc.issuerUrl=https://accounts.google.com \
--set config.ui.auth.oidc.clientId=MY_CLIENT_ID \
--set config.ui.auth.oidc.clientSecret=MY_CLIENT_SECRET \
--set "config.ui.auth.oidc.redirectUrl=https://elara.example.com/auth/callback" \
--set config.ui.auth.oidc.adminEmail=admin@example.com
See OIDC Setup for how the admin bootstrap and anti-hijack guard work.
With production-grade resources + persistence
# values-prod.yaml
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { cpu: "2", memory: 1Gi }
persistence:
size: 50Gi
storageClassName: ssd
metrics:
enabled: true
serviceMonitor:
enabled: true
tracing:
enabled: true
otlpEndpoint: http://otel-collector.observability:4318
ingress:
enabled: true
className: nginx
hosts:
- host: elara.example.com
paths:
- { path: /, pathType: Prefix, port: http }
tls:
- secretName: elara-tls
hosts: [elara.example.com]
helm install elara ./helm/elara -f values-prod.yaml
Values reference
Full list with descriptions lives in
values.yaml.
Key sections:
| Key | Default | Purpose |
|---|---|---|
image.repository |
ghcr.io/sergeyslonimsky/elara |
Container image |
image.tag |
Chart appVersion |
Pin a specific version |
image.digest |
"" |
Overrides tag for immutable deploys |
replicaCount |
1 |
Invariant: schema pins to 1 until raft HA lands |
config.ui.server.port |
8080 |
HTTP/2, ConnectRPC, Web UI |
config.ui.server.writeTimeout |
24h |
Server-streaming RPCs need a long write timeout |
config.ui.auth.enabled |
false |
Enable authentication; all RPCs are public when false |
config.ui.auth.type |
basic-auth |
basic-auth, oidc, or none. The chart always emits UI_AUTH_TYPE, even with auth disabled — which is why the service treats an empty value as none rather than as a typo. An unrecognized value fails startup. |
config.ui.auth.basicAuth.username |
"" |
Bootstrap admin email (required for basic-auth) |
config.ui.auth.basicAuth.password |
"" |
Bootstrap admin password (basic-auth); stored in chart Secret |
config.ui.auth.oidc.issuerUrl |
"" |
OIDC issuer (e.g. https://accounts.google.com) |
config.ui.auth.oidc.clientId |
"" |
OIDC client ID |
config.ui.auth.oidc.clientSecret |
"" |
OIDC client secret; stored in chart Secret |
config.ui.auth.oidc.redirectUrl |
"" |
OIDC callback URL |
config.ui.auth.oidc.scopes |
[] |
OIDC scopes; defaults to [openid, email, profile] |
config.ui.auth.oidc.adminEmail |
"" |
Bootstrap admin email; pre-provisioned as superadmin placeholder, claimed by first matching OIDC login (required for OIDC) |
config.ui.auth.session.secureCookie |
true |
Add Secure flag to session cookie; disable only for HTTP dev |
config.ui.auth.existingSecret |
"" |
Use a pre-existing Secret instead of the chart-managed one |
config.client.etcd.port |
2379 |
etcd-compatible gRPC API |
config.client.auth.enabled |
false |
Bearer-token auth on etcd gRPC + mounts the TokenService |
config.client.history.* |
see values.yaml |
Connected-clients history tuning |
config.client.recentEvents.* |
see values.yaml |
Recent-events ring buffer tuning |
storage.type |
bbolt |
Schema enum: [bbolt] today |
storage.bbolt.path |
/var/lib/elara |
Directory inside the PVC mount |
persistence.size |
2Gi |
PVC size via volumeClaimTemplates |
persistence.accessMode |
ReadWriteOnce |
bbolt requires exclusive lock |
metrics.enabled |
false |
Exposes /metrics on the HTTP port |
metrics.serviceMonitor.enabled |
false |
Requires Prometheus Operator CRDs |
tracing.enabled |
false |
OTLP push |
tracing.otlpEndpoint |
"" |
Required when tracing.enabled=true |
config.log.level |
info |
One of: debug, info, warn, error |
config.log.format |
json |
One of: json, text |
config.log.noSource |
false |
Set true to omit source file/line from log entries |
service.type |
ClusterIP |
NodePort/LoadBalancer supported |
ingress.enabled |
false |
Exposes HTTP port only |
networkPolicy.enabled |
false |
Optional; CNI-dependent |
How configuration reaches the service
values.yaml → ConfigMap (env vars) → service reads them through Viper,
same mapping rule as everywhere else in Elara — see
Configuration → Environment-variable naming.
Sensitive values (basicAuth.password, oidc.clientSecret) go into a Secret
instead of the ConfigMap. Session lifetime is not a chart value: sessions are
opaque server-side ids with compile-time TTLs — see
Sessions & Tokens. Add extra env-vars via extraEnv or
wire a Secret with extraEnvFrom.
gRPC exposure
The etcd-compatible gRPC API on port 2379 is not routed through the default HTTP Ingress. Common patterns:
- Cluster-internal only (default): consume via the ClusterIP service.
elara.{namespace}.svc.cluster.local:2379
- External via LoadBalancer: set
service.type=LoadBalancer; clients connect directly to the LB IP on port 2379. - External via gRPC-aware ingress: create a dedicated Ingress or Gateway
resource pointing at the
grpcport. Example with nginx-ingress:
metadata:
annotations:
nginx.ingress.kubernetes.io/backend-protocol: GRPC
spec:
rules:
- host: etcd.elara.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: elara
port: { name: grpc }
Not shipped in the chart by default — add it via extraObjects or a
sibling manifest.
Persistence notes
- The PVC is created via StatefulSet
volumeClaimTemplates.helm uninstalldoes not delete the PVC — delete it manually when you want to wipe data. - For existing storage, set
persistence.existingClaimand the chart will mount that PVC instead of creating one. This works only withreplicaCount: 1. persistence.enabled: falseusesemptyDir— data is lost on pod restart. Use only for dev or CI.
Roadmap
| Feature | Impact on chart |
|---|---|
| Raft-based HA | Ships HPA template, relaxes replicaCount schema, adds PDB |
| PostgreSQL storage backend | Unlocks storage.postgres.* + migrations init container |
| S3 storage backend | Unlocks storage.s3.* |
Testing
helm lint ./helm/elara
helm template elara ./helm/elara # render templates
helm install elara ./helm/elara --dry-run --debug # schema + template dry run
helm test elara # in-cluster smoke test