Helm charts for deploying Earthly Lunar on Kubernetes.
- Kubernetes 1.29+
- Helm 3.x
- PostgreSQL 16+ (external — not included in the chart)
- S3-compatible object storage (two buckets: logs and resources)
- A GitHub App installed on your organization — the fastest path to create one is our manifest script (browser-based flow, prints the credentials you'll need below)
helm repo add earthly https://earthly.github.io/charts
helm repo update
helm install lunar earthly/lunar \
--namespace lunar --create-namespace \
-f values.yamlBefore the command above will succeed, create the required secrets and set the required values.
The chart requires three user-managed secrets up front: the database credentials, the GitHub App private key, and the Hub licence JWT. Three other secrets (the hub auth token, the GitHub webhook secret, and — when enabled — the Grafana admin credentials) are auto-generated by the chart on install with a random value, persisted as Kubernetes Secrets with helm.sh/resource-policy: keep, and re-read from the cluster on subsequent upgrades. To bring your own instead of the auto-generated value, set the secretName field on the corresponding values key — the chart will reference your secret and skip auto-generation. See the GitOps note below if you use ArgoCD or Flux.
# Database credentials (always user-managed)
kubectl -n lunar create secret generic lunar-db \
--from-literal=username='<db-user>' \
--from-literal=password='<db-password>'
# GitHub App private key (always user-managed).
# The hub base64-decodes this value internally, so the secret must contain the
# base64-encoded PEM (not the raw PEM). The one-liner below works on both
# GNU and BSD/macOS base64.
kubectl -n lunar create secret generic lunar-github-app \
--from-literal=private-key="$(base64 < path/to/private-key.pem | tr -d '\n')"
# Hub licence JWT (always user-managed).
kubectl -n lunar create secret generic lunar-hub-licence \
--from-literal=hub-licence.jwt='<signed-licence-jwt>'After install, retrieve any chart-managed secret with:
# Hub auth token (pass to CLI / CI agents as LUNAR_HUB_TOKEN)
kubectl -n lunar get secret lunar-auth-token \
-o jsonpath='{.data.token}' | base64 -d
# GitHub webhook secret (register with your GitHub App)
kubectl -n lunar get secret lunar-github-webhook \
-o jsonpath='{.data.webhook-secret}' | base64 -d(Adjust lunar-... for your release name; the actual names are <release>-auth-token, <release>-github-webhook, <release>-grafana-admin.)
Helm's lookup function — used by the chart to persist auto-generated secrets across upgrades — returns empty during client-side template rendering. ArgoCD and Flux render Helm charts client-side by default, which means without a workaround they would regenerate the random values on every reconciliation and detect continuous drift.
Two options if you use GitOps:
-
Bring-your-own-secrets — set
hub.auth.secretName,hub.github.webhookSecret.secretName, andgrafana.admin.secretNameto secrets you manage with External Secrets Operator, Sealed Secrets, Vault, etc. The chart will reference them and skip auto-generation entirely. -
Tell your GitOps tool to ignore the generated fields — example for ArgoCD:
ignoreDifferences: - kind: Secret jsonPointers: - /data/token - /data/webhook-secret - /data/password
Minimum values.yaml that has to be provided — everything else has a sensible default.
hub:
licence:
secretName: "lunar-hub-licence"
secretKey: "hub-licence.jwt"
db:
name: lunar
host: your-db-host.example.com
s3:
logsBucket: your-lunar-logs-bucket
resourcesBucket: your-lunar-resources-bucket
github:
app:
id: 123456
installId: 78901234Plus two URL prerequisites the chart needs to wire correctly:
-
Hub webhook URL — for GitHub webhook registration to work, the hub needs an externally-reachable URL. The simplest path is to let the chart manage your ingress and set
hub.ingress.webhooks.host— see Ingress below. Whenhub.ingress.enabled: true, the chart deriveshttps://<webhooks.host>automatically. BYO-ingress installs (hub.ingress.enabled: false) must sethub.webhookURLexplicitly — the chart will not guess a URL it doesn't route to. -
Grafana URL (when
grafana.enabled: true, which is the default) — Grafana needs to know its own external URL for OIDCredirect_uriand absolute link rendering. Pick one:grafana.ingress.enabled: truewithgrafana.ingress.hosts[0].hostset — chart derives the URL automatically.grafana.externalURL: "https://grafana.example.com"— explicit override, for BYO ingress / Caddy / shared LB with path routing.grafana.enabled: false— skip Grafana entirely.
The chart fails fast at install time if none of these are set (it deliberately won't guess at a URL it doesn't route to — wrong host means broken OIDC, silently).
Hub authenticates to GitHub as a GitHub App. Two modes, mutually exclusive:
-
Single-App (default). Set
hub.github.app.owner(the GitHub org or user the App is installed on),hub.github.app.id, andhub.github.app.installId, then create thelunar-github-appSecret holding the App's private-key PEM. Suitable for single-tenant deployments where the Hub fronts one App installed in one org. Required as of 2.2.0:hub.github.app.owneris now required — prior chart versions inferred a default routing internally; the Hub now requires the value explicitly viaHUB_GITHUB_APP_OWNER. -
Multi-App. Use this when the Hub serves multiple orgs that each install their own Lunar App. List one entry per owner under
hub.github.apps, and put all the PEM files in a single Kubernetes Secret named viahub.github.appsSecret.secretName. The chart looks up each entry's PEM at<lowercase-owner>.peminside that Secret.hub: github: apps: - owner: earthly appId: 123 installId: 100 - owner: acme appId: 456 installId: 200 appsSecret: secretName: lunar-github-apps
Create the Secret out of band:
kubectl create secret generic lunar-github-apps \ --from-file=earthly.pem=./earthly.pem \ --from-file=acme.pem=./acme.pem
The chart validates the chosen mode at install time with helm.sh/fail (mutex, required fields, duplicate owners).
Lunar uses two S3-compatible buckets — one for streaming script logs, one for script resource archives fetched by init containers. Both must exist and be writable before pods start doing real work.
AWS credentials are intentionally out of scope for this chart. The hub uses the standard AWS SDK credential chain, so you can use whichever mechanism fits your cluster:
-
IRSA (recommended on EKS) — annotate the chart's service account with the role ARN. The role needs
s3:GetObjectands3:PutObjecton both buckets.serviceAccount: annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/lunar-hub-s3 hub: extraEnv: - name: AWS_REGION value: us-east-1
-
Explicit env vars — inject credentials via
hub.extraEnv, sourced from an existing secret:hub: extraEnv: - name: AWS_REGION value: us-east-1 - name: AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: { name: lunar-aws, key: access-key-id } - name: AWS_SECRET_ACCESS_KEY valueFrom: secretKeyRef: { name: lunar-aws, key: secret-access-key }
Other providers (GKE Workload Identity, pod identity, IMDS, etc.) all work the same way — set whatever AWS_* or service-account plumbing you'd normally use for any AWS-SDK workload. The chart does not create the buckets for you.
Only create these if you need the features they enable.
# Per-scope runtime secrets (only when the matching hub.secrets.*.secretName is set).
# The k8s secret's `secrets` data key is parsed by Hub as a comma-separated list
# of `NAME:VALUE` pairs (kelseyhightower/envconfig map format — NOT JSON). Each
# pair surfaces in script pods as `LUNAR_SECRET_<NAME>`.
# Most installs don't need these — prefer per-type script container spec envFrom /
# volumes (see operator.scriptContainerSpec*) for fine-grained control.
kubectl -n lunar create secret generic lunar-collector-secrets --from-literal=secrets='GH_TOKEN:ghp_xxx,NPM_TOKEN:npm_yyy'
kubectl -n lunar create secret generic lunar-cataloger-secrets --from-literal=secrets='GH_TOKEN:ghp_xxx'
kubectl -n lunar create secret generic lunar-policy-secrets --from-literal=secrets='SLACK_WEBHOOK_URL:https://hooks.slack.com/services/...'
# Grafana admin (only if you set grafana.admin.secretName instead of letting
# the chart auto-generate; both username and password live in one secret)
kubectl -n lunar create secret generic my-grafana-admin \
--from-literal=username='admin' \
--from-literal=password='<your-grafana-password>'The hub has two trust boundaries:
- Trusted API clients — lunar CLI, CI agents, operator. Talk to hub over gRPC (port 8000) and HTTP (
/logson port 8001). Both use the same Hub auth token. - GitHub webhooks — public internet. POST to
/webhookson port 8001 only.
The chart renders separate ingresses for each, configured under two logical blocks: hub.ingress.api and hub.ingress.webhooks. You can put the API ingress on a private hostname (Tailscale, internal-DNS, VPN) and only expose webhooks to the public internet — or use the same hostname for both. Disable entirely (hub.ingress.enabled: false) if you front the hub with a LoadBalancer Service, a service mesh, or your own Ingress YAML.
Single-host install (same hostname serves both — fine for most setups):
hub:
ingress:
enabled: true
className: nginx
tls:
- secretName: lunar-tls
hosts:
- lunar.example.com
annotations:
cert-manager.io/cluster-issuer: letsencrypt
api:
host: lunar.example.com
# NGINX needs backend-protocol: GRPC on the gRPC Ingress. Other
# controllers use their own equivalent (ALB: backend-protocol-version:
# GRPC; Traefik: h2c per-service; etc).
grpcAnnotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
webhooks:
host: lunar.example.comSplit-host install (API stays internal, webhooks ingress is the only public surface):
hub:
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
api:
host: api.lunar.example.com
tls:
- secretName: lunar-api-tls
hosts:
- api.lunar.example.com
# Per-sub-Ingress annotations. Repeat the whitelist on both so it
# applies to api-grpc AND api-http; the chart intentionally does not
# apply it to webhooks (different trust boundary).
grpcAnnotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8"
httpAnnotations:
nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8"
webhooks:
host: webhooks.lunar.example.com
tls:
- secretName: lunar-webhooks-tls
hosts:
- webhooks.lunar.example.comWhen the hub boots, it reads hub.webhookURL (derived as https://<hub.ingress.webhooks.host> when hub.ingress.enabled: true; must be set explicitly otherwise) and registers <webhookURL>/webhooks/github with the GitHub App. GitHub then POSTs every webhook event to that URL, which must resolve to the webhooks ingress.
sequenceDiagram
participant Hub as Hub<br/>(reads webhookURL)
participant GH as GitHub App
participant DNS
participant Ingress as Webhooks Ingress<br/>(listens on webhooks.host)
participant HubPod as Hub Pod<br/>:8001
Hub->>GH: "Register webhook at <webhookURL>/webhooks/github"
Note over Hub,GH: One-time, at hub boot
Note over GH: Later: a PR event happens
GH->>DNS: Resolve <webhookURL host>
DNS-->>GH: Cluster LB IP
GH->>Ingress: POST /webhooks/github
Ingress->>HubPod: Forward
HubPod-->>Ingress: 200 OK
Ingress-->>GH: 200 OK
Three things must agree, or webhooks land in the void:
- The hostname inside
hub.webhookURL(derived fromwebhooks.hostwhenhub.ingress.enabled: true; set explicitly otherwise). - The DNS record for that hostname must resolve to your cluster's ingress.
- An Ingress with a rule for that hostname and
/webhookspath — which the chart creates for you whenhub.ingress.enabled: true; BYO installs are responsible for routing themselves.
The chart enforces #1 internally: if you set hub.webhookURL explicitly alongside chart-managed ingress, its host portion must equal hub.ingress.webhooks.host. Install fails fast if they disagree.
Three Ingress resources, all in the release namespace:
| Resource | Host | Path | Backend port |
|---|---|---|---|
<release>-hub-api-grpc |
api.host |
/ |
hub gRPC (8000) |
<release>-hub-api-http |
api.host |
/logs |
hub HTTP (8001) |
<release>-hub-webhooks |
webhooks.host |
/webhooks |
hub HTTP (8001) |
The api-grpc and api-http split exists because most ingress controllers apply backend-protocol annotations per-Ingress.
The ingress shape changed in chart 2.0.0:
hub.publicBaseURLwas renamed tohub.webhookURL, and is now optional whenhub.ingress.enabled: true— defaults tohttps://<webhooks.host>in that case. BYO-ingress installs must set it explicitly (the chart will not derive a URL it doesn't route to). Also set explicitly for non-default scheme/port or a path prefix.hub.ingress.hostmoved tohub.ingress.api.hostandhub.ingress.webhooks.host(set both to the same value for a single-host install).hub.ingress.grpcAnnotations/httpAnnotationsmoved underhub.ingress.api.*.hub.ingress.api.grpcAnnotationsno longer defaults to NGINX'sbackend-protocol: "GRPC". NGINX users must set it explicitly (see Ingress examples); other controllers set their equivalent.hub.grafanaURLBasewas renamed tografana.externalURL. It's a Grafana property — Hub consumes it asHUB_GRAFANA_URL_BASE, Grafana consumes it asGF_SERVER_ROOT_URL. Lives undergrafana.*now, where it belongs.- Grafana URL derivation changed. In 1.x,
HUB_GRAFANA_URL_BASEand Grafana's ownGF_SERVER_ROOT_URLdefaulted topublicBaseURL. 2.0.0 only derives a Grafana URL when the chart actually controls Grafana's routing (chart-managedgrafana.ingressor explicitgrafana.externalURL). If you used a single hostname withgrafana.ingress.enabled: falseand external path-routing (Caddy / nginx-ingress with split paths / similar), setgrafana.externalURLto that same URL explicitly — otherwise Grafana's OIDCredirect_uriand absolute links break after upgrade. The chart fails fast at install time whengrafana.enabled: trueand no Grafana URL can be determined.
The chart fails fast at install time when it sees the old shape.
# Before (chart 1.x)
hub:
publicBaseURL: "https://lunar.example.com"
ingress:
enabled: true
host: lunar.example.com
grpcAnnotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
# After (chart 2.0.0) — single-host
hub:
# webhookURL derived as "https://lunar.example.com" automatically.
ingress:
enabled: true
api:
host: lunar.example.com
grpcAnnotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
webhooks:
host: lunar.example.com
# After (chart 2.0.0) — split-host
hub:
# webhookURL derived as "https://webhooks.lunar.example.com".
ingress:
enabled: true
api:
host: api.lunar.example.com
grpcAnnotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
webhooks:
host: webhooks.lunar.example.com
# After (chart 2.0.0) — BYO ingress (LoadBalancer / mesh / your own YAML).
# The chart wires HUB_PUBLIC_BASE_URL, HUB_GRAFANA_URL_BASE, and Grafana's
# GF_SERVER_ROOT_URL from the values below — set whichever apply. You own
# routing GitHub's POSTs to the hub-http Service (named `<release>-hub:8001`)
# and routing browser traffic to the grafana Service. The chart only emits
# these env vars when the corresponding value is set; unset means the
# component falls back to its own default (or warns at boot).
hub:
webhookURL: "https://lunar.example.com"
ingress:
enabled: false
grafana:
# Required when grafana.enabled is true and the chart isn't managing
# Grafana's ingress — otherwise the install fails fast at template time
# (GF_SERVER_ROOT_URL empty → OIDC redirect_uris break).
externalURL: "https://grafana.example.com"Install and configure the Lunar CLI (needs LUNAR_HUB_HOST and LUNAR_HUB_TOKEN at minimum), then pull your primary configuration into the Hub:
lunar hub pull github://your-org/your-config-repo@mainThe Hub automatically registers per-repo GitHub webhooks at <hub.webhookURL>/webhooks/github when manifests are pulled. No manual webhook configuration is required as long as:
hub.webhookURLresolves to your webhooks ingress and is reachable from GitHub. When chart-managed ingress is enabled, this is derived fromhub.ingress.webhooks.hostautomatically.- The GitHub App has the
repository_hooks: writepermission (the manifest script grants this by default).
helm repo update
helm upgrade lunar earthly/lunar \
--namespace lunar \
-f values.yamlPer-release changes (breaking changes, new values, behaviour shifts)
are recorded in the chart CHANGELOG. Read
the entries between your current version and the one you're upgrading
to before running helm upgrade.
helm uninstall lunar --namespace lunarThis removes all Kubernetes resources created by the chart. The PVC for hub state is not deleted automatically — remove it manually if you want to discard all data.
Run helm show values earthly/lunar for the full, authoritative list. Defaults below are grouped by component.
Global
| Key | Description | Default |
|---|---|---|
nameOverride |
Override the chart name | "" |
fullnameOverride |
Override the full release name | "" |
clusterDomain |
Kubernetes cluster DNS domain. Override only if your cluster was provisioned with a non-default --cluster-domain |
cluster.local |
logging.level |
Log level (debug, info, warn, error) applied to both Hub and Operator |
info |
logging.format |
Log format (json or text) applied to both Hub and Operator |
json |
imagePullSecrets |
Image pull secrets for all pods | [] |
serviceAccount.create |
Create a service account | true |
serviceAccount.automount |
Automount the service account token | true |
serviceAccount.annotations |
Service account annotations (e.g. IAM role ARN) | {} |
serviceAccount.name |
Service account name (auto-generated if empty) | "" |
Hub
The central gRPC/HTTP server. Stores metadata, evaluates policies, and serves the API.
Image
| Key | Description | Default |
|---|---|---|
hub.image.repository |
Hub container image | ghcr.io/earthly/lunar-hub |
hub.image.tag |
Image tag | 2.1.1 |
hub.image.pullPolicy |
Pull policy | IfNotPresent |
hub.maxWorkers.collect |
Max Hub workers for collector queue jobs; 0 means unlimited |
10 |
hub.maxWorkers.policy |
Max Hub workers for policy queue jobs; 0 means unlimited |
20 |
hub.maxWorkers.cronCollect |
Max Hub workers for cron collector queue jobs; 0 means unlimited |
5 |
hub.maxWorkers.cataloger |
Max Hub workers for cataloger queue jobs; 0 means unlimited |
1 |
hub.extraEnv |
Additional environment variables (name/value or valueFrom pairs) |
[] |
Public URL
| Key | Description | Default |
|---|---|---|
hub.webhookURL |
External URL where GitHub posts webhooks. Chart registers <webhookURL>/webhooks/github with the GitHub App at boot. Defaults to https://<hub.ingress.webhooks.host> only when hub.ingress.enabled: true — the chart only derives a URL when it actually routes the traffic. Set explicitly when ingress is disabled (BYO) or you need a non-default scheme/port/path. When set explicitly alongside chart-managed ingress, the host portion must equal hub.ingress.webhooks.host — install fails fast otherwise. |
"" (derived) |
See also grafana.externalURL (under Grafana) for the Grafana-side equivalent — drives both HUB_GRAFANA_URL_BASE (consumed by Hub) and GF_SERVER_ROOT_URL (consumed by Grafana).
Licence
| Key | Description | Default |
|---|---|---|
hub.licence.secretName |
Secret containing the signed Hub licence JWT | lunar-hub-licence |
hub.licence.secretKey |
Key within the licence secret | hub-licence.jwt |
hub.licence.filePath |
In-container path used for HUB_LICENCE_FILE |
/var/run/secrets/lunar/hub-licence.jwt |
Database
| Key | Description | Default |
|---|---|---|
hub.db.host |
PostgreSQL host | "" (required) |
hub.db.name |
Database name | "" (required) |
hub.db.port |
PostgreSQL port | 5432 |
hub.db.waitSecs |
Seconds to wait for DB readiness on startup | 45 |
hub.db.user.secretName |
Secret containing the DB username | lunar-db |
hub.db.user.secretKey |
Key within the secret | username |
hub.db.pass.secretName |
Secret containing the DB password | lunar-db |
hub.db.pass.secretKey |
Key within the secret | password |
hub.db.connectionOptions |
Extra options appended to the Postgres connection string (libpq KV format, space-separated). Default works against managed Postgres with forced TLS (RDS, Aurora, Cloud SQL); set to "sslmode=disable" for plain cluster-local Postgres. Also consumed by the operator. |
"sslmode=require" |
Override footgun: setting
hub.db.connectionOptionsreplaces the whole string — the default is not merged in. If passing additional options (connect_timeout,application_name, etc.), includesslmode=yourself, space-separated:# OK hub: db: connectionOptions: "sslmode=require connect_timeout=10" # BAD — silently drops sslmode=require hub: db: connectionOptions: "connect_timeout=10"
GitHub
Hub authenticates as a GitHub App. App ID, install ID, and the App's private-key secret are all required — the chart fails at install time otherwise. See GitHub authentication.
| Key | Description | Default |
|---|---|---|
hub.github.app.owner |
GitHub org or user the App is installed on (single-App mode) (required as of 2.2.0) | "" |
hub.github.app.id |
GitHub App ID (single-App mode) | 0 |
hub.github.app.installId |
GitHub App Installation ID (single-App mode) | 0 |
hub.github.app.privateKey.secretName |
Secret containing the App private-key PEM (single-App mode) | lunar-github-app |
hub.github.app.privateKey.secretKey |
Key within the secret | private-key |
hub.github.apps |
Multi-App entries: list of {owner, appId, installId}. Mutually exclusive with hub.github.app.*. |
[] |
hub.github.appsSecret.secretName |
Secret holding one PEM key per apps[].owner (key name: <lowercase-owner>.pem) |
lunar-github-apps |
hub.github.webhookSecret.secretName |
Secret containing the webhook secret | lunar-github-webhook |
hub.github.webhookSecret.secretKey |
Key within the secret | webhook-secret |
hub.github.baseUrl |
GitHub API base URL (for GitHub Enterprise Server) | "" |
hub.github.syncWindow |
How far back to sync GitHub data on first pull | 2160h (90 days) |
S3 / Object Storage
| Key | Description | Default |
|---|---|---|
hub.s3.logsBucket |
S3 bucket for log storage | "" (required) |
hub.s3.resourcesBucket |
S3 bucket for script resources | "" (required) |
hub.s3.logsUrlTtl |
Pre-signed URL TTL for script log uploads — Go duration string | 5m |
hub.s3.resourcesUrlTtl |
Pre-signed URL TTL for script resource downloads (init-container fetch) | 1h |
Auth
| Key | Description | Default |
|---|---|---|
hub.auth.secretName |
Secret containing the Hub auth token | lunar-auth-token |
hub.auth.secretKey |
Key within the secret | token |
Script secrets (optional)
Per-scope secrets the hub forwards to script execution. Each scope (collector / cataloger / policy) supports two delivery shapes; pick one per scope.
Default — single key (perKey: false): the K8s Secret's secretKey data entry is a comma-separated list of NAME:VALUE pairs (envconfig map format, not JSON); each pair surfaces as LUNAR_SECRET_<NAME> in the script pod. Simplest shape, but all keys travel together: rotating one requires re-supplying the rest.
Per-key (perKey: true): every data key in the K8s Secret is mounted via envFrom: secretRef + prefix: and surfaces as HUB_<SCOPE>_SECRET_<KEY>=<value> in the hub, then re-emitted to scripts as LUNAR_SECRET_<KEY>. Operators can add or rotate a single key with kubectl edit secret / kubectl patch without touching the others — this is the recommended shape going forward. Requires hub >= 2.2.0. The hub merges both shapes if both are configured (per-key wins on conflict), so migrating one key at a time is safe.
Most installs don't need these — per-type container spec envFrom / volumes on operator.scriptContainerSpec* is usually a cleaner path. Leave secretName empty to skip injection entirely.
| Key | Description | Default |
|---|---|---|
hub.secrets.collector.secretName |
Collector secrets; empty disables | "" |
hub.secrets.collector.secretKey |
Key within the secret (single-key shape only) | secrets |
hub.secrets.collector.perKey |
Mount via envFrom + prefix: HUB_COLLECTOR_SECRET_ (per-key shape) |
false |
hub.secrets.cataloger.secretName |
Cataloger secrets; empty disables | "" |
hub.secrets.cataloger.secretKey |
Key within the secret (single-key shape only) | secrets |
hub.secrets.cataloger.perKey |
Mount via envFrom + prefix: HUB_CATALOGER_SECRET_ (per-key shape) |
false |
hub.secrets.policy.secretName |
Policy secrets; empty disables | "" |
hub.secrets.policy.secretKey |
Key within the secret (single-key shape only) | secrets |
hub.secrets.policy.perKey |
Mount via envFrom + prefix: HUB_POLICY_SECRET_ (per-key shape) |
false |
Logging
Hub logging uses the top-level global logging.* values. Tenant and telemetry routing config is loaded from the signed licence JWT mounted via hub.licence.*.
Policy queue
| Key | Description | Default |
|---|---|---|
hub.policyQueue.pollInterval |
How often the queue is polled | 1s |
hub.policyQueue.numWorkers |
Number of concurrent policy evaluation workers | 5 |
Persistence
The Hub uses a PVC for state, cached repos, and script code.
| Key | Description | Default |
|---|---|---|
hub.persistence.enabled |
Create a PVC for hub state | true |
hub.persistence.storageClass |
StorageClass (empty = cluster default) | "" |
hub.persistence.size |
Volume size | 10Gi |
hub.persistence.accessModes |
PVC access modes | [ReadWriteOnce] |
Networking
| Key | Description | Default |
|---|---|---|
hub.service.type |
Service type | ClusterIP |
hub.service.ports.server |
gRPC port | 8000 |
hub.service.ports.http |
HTTP port | 8001 |
hub.ingress.enabled |
Render the hub ingresses (three resources: api-grpc, api-http, webhooks) | false |
hub.ingress.className |
Shared default ingress class. Overridden per-block when set. | "" |
hub.ingress.tls |
Shared default TLS config. Overridden per-block when set. | [] |
hub.ingress.annotations |
Shared default annotations applied to all three ingresses. | {} |
hub.ingress.api.host |
Hostname for the API ingress (gRPC + /logs). Required when enabled. |
"" |
hub.ingress.api.className |
Override ingress.className for the API ingress. |
"" |
hub.ingress.api.tls |
Override ingress.tls for the API ingress. |
[] |
hub.ingress.api.grpcAnnotations |
Annotations applied only to the api-grpc Ingress, layered on top of ingress.annotations. NGINX users need backend-protocol: GRPC here (other controllers have their own equivalent — see Ingress for examples). Controller-neutral default — set explicitly for your ingress class. |
{} |
hub.ingress.api.httpAnnotations |
Annotations applied only to the api-http (/logs) Ingress, layered on top of ingress.annotations. |
{} |
hub.ingress.webhooks.host |
Hostname for the webhooks ingress (GitHub /webhooks). Required when ingress.enabled: true. Source of the derived hub.webhookURL (only in that case — when ingress is disabled the chart does not derive from this field). |
"" |
hub.ingress.webhooks.className |
Override ingress.className for the webhooks ingress. |
"" |
hub.ingress.webhooks.tls |
Override ingress.tls for the webhooks ingress. |
[] |
hub.ingress.webhooks.annotations |
Annotations layered on top of ingress.annotations for the webhooks ingress. |
{} |
Probes
| Key | Description | Default |
|---|---|---|
hub.readinessProbe.enabled |
Enable readiness probe | true |
hub.readinessProbe.initialDelaySeconds |
Delay before first check | 0 |
hub.readinessProbe.periodSeconds |
Check interval | 5 |
hub.readinessProbe.failureThreshold |
Failures before unready | 3 |
hub.livenessProbe.enabled |
Enable liveness probe | true |
hub.livenessProbe.initialDelaySeconds |
Delay before first check | 0 |
hub.livenessProbe.periodSeconds |
Check interval | 5 |
hub.livenessProbe.failureThreshold |
Failures before restart | 3 |
Scheduling & pod spec
| Key | Description | Default |
|---|---|---|
hub.resources |
CPU/memory requests and limits | {} |
hub.nodeSelector |
Node selector | {} |
hub.tolerations |
Tolerations | [] |
hub.affinity |
Affinity rules | {} |
hub.labels |
Additional deployment labels | {} |
hub.annotations |
Additional deployment annotations | {} |
hub.podLabels |
Additional pod labels | {} |
hub.podAnnotations |
Additional pod annotations | {} |
hub.podSecurityContext |
Pod security context | {} |
hub.securityContext |
Container security context | {} |
hub.volumeMounts |
Additional volume mounts | [] |
hub.volumes |
Additional volumes | [] |
Operator
Watches for script execution jobs and creates Kubernetes pods to run them.
Images
| Key | Description | Default |
|---|---|---|
operator.image.repository |
Operator image | ghcr.io/earthly/lunar-snippet-operator |
operator.image.tag |
Image tag | 2.1.1 |
operator.image.pullPolicy |
Pull policy | IfNotPresent |
operator.initImage.repository |
Init container image | ghcr.io/earthly/lunar-snippet-init |
operator.initImage.tag |
Image tag | 2.1.1 |
operator.sidecarImage.repository |
Sidecar container image | ghcr.io/earthly/lunar-snippet-sidecar |
operator.sidecarImage.tag |
Image tag | 2.1.1 |
Behavior
| Key | Description | Default |
|---|---|---|
operator.scriptNamespace |
Namespace for script pods (must exist if set) | "" (release namespace) |
operator.hubHost |
Override hostname the operator and script pods use to reach Hub gRPC. Empty = computed in-cluster FQDN, which resolves cross-namespace. Set only for service-mesh / split-DNS / multi-cluster topologies | "" |
operator.maxConcurrent |
Max concurrent script pods | 10 |
operator.healthPort |
Operator health check port | 8081 |
operator.extraEnv |
Additional environment variables | [] |
Script pod configuration
| Key | Description | Default |
|---|---|---|
operator.scriptContainerSpecPolicy |
Base container spec for policy script pods (resources, securityContext, env, etc.) | {} |
operator.scriptContainerSpecCollector |
Base container spec for collector script pods | {} |
operator.scriptContainerSpecCataloger |
Base container spec for cataloger script pods | {} |
operator.batchMaxCountPolicy |
Max jobs per policy pod; 0 uses the operator default |
0 |
operator.batchMaxCountCollector |
Max jobs per collector pod; 0 uses the operator default |
0 |
operator.batchMaxCountCataloger |
Max jobs per cataloger pod; 0 uses the operator default |
0 |
operator.scriptPodNodeSelector |
Node selector for script pods | {} |
operator.scriptPodTolerations |
Tolerations for script pods | [] |
Logging
Operator logging uses the top-level global logging.* values. Tenant and telemetry routing config is fetched at runtime from the Hub (GetRuntimeConfig).
Scheduling & pod spec
| Key | Description | Default |
|---|---|---|
operator.resources |
CPU/memory requests and limits | {} |
operator.nodeSelector |
Node selector | {} |
operator.tolerations |
Tolerations | [] |
operator.affinity |
Affinity rules | {} |
operator.podLabels |
Additional pod labels | {} |
operator.podAnnotations |
Additional pod annotations | {} |
operator.podSecurityContext |
Pod security context | {} |
operator.securityContext |
Container security context | {} |
Grafana
Pre-built Grafana instance with dashboards for policy results, component health, and collection activity. Deployed by default; set grafana.enabled: false to opt out.
| Key | Description | Default |
|---|---|---|
grafana.enabled |
Deploy the pre-built Grafana instance | true |
grafana.externalURL |
External URL where Grafana is reachable. Drives GF_SERVER_ROOT_URL (Grafana's self-knowledge — used for OIDC redirect_uri, absolute link rendering, etc) and HUB_GRAFANA_URL_BASE ([More Details] links in PR comments). Defaults to https://<grafana.ingress.hosts[0].host> when chart-managed Grafana ingress is enabled. Empty otherwise — the chart only derives a URL when it actually controls the routing (no fallback to hub.webhookURL or hub.ingress.api.host — wrong trust boundary). Installs with externally-managed Grafana routing must set this explicitly; otherwise install fails fast. |
"" (derived) |
grafana.image.repository |
Grafana image | ghcr.io/earthly/lunar-grafana |
grafana.image.tag |
Image tag | 2.1.1 |
grafana.admin.secretName |
Secret containing both admin credentials. Empty = chart auto-generates <release>-grafana-admin (kept across uninstall) |
"" |
grafana.admin.userKey |
Key within the secret holding the username | username |
grafana.admin.passwordKey |
Key within the secret holding the password | password |
grafana.service.type |
Service type | ClusterIP |
grafana.service.port |
Service port | 80 |
grafana.ingress.* |
Same structure as hub.ingress.* |
disabled |
grafana.extraEnv |
Additional environment variables | [] |
grafana.resources |
CPU/memory requests and limits | {} |
grafana.nodeSelector |
Node selector | {} |
grafana.tolerations |
Tolerations | [] |
grafana.affinity |
Affinity rules | {} |