For the complete documentation index, see llms.txt. This page is also available as Markdown.

Install Walkthrough

This walkthrough takes you from zero to a working Lunar Hub. It assumes you've worked through the prerequisites, and have:

  • Kubernetes cluster with your namespaces created

  • Postgres database

  • two S3 buckets

  • GitHub App

  • hub licence key

  • DNS name for the Hub

Assuming your prerequisites are in order, expect the install to take 15–30 minutes end to end. The bulk of that is waiting for external systems (e.g. first-boot migrations).

Step 1 β€” Add the Helm repo

helm repo add earthly https://earthly.github.io/charts
helm repo update

Verify:

helm search repo earthly/lunar

You should see the chart listed with a version number.

Step 2 β€” Verify your prerequisites

Before going further, sanity-check the external dependencies you provisioned in the prerequisites. An issue here is cheaper to find now than after you've created secrets and written your values.yaml.

DNS (prereqs Step 2):

Should resolve to your ingress controller's external IP. If it doesn't yet, either wait or add an A / CNAME record now β€” GitHub webhook auto-registration needs a publicly reachable URL.

Postgres reachability (step 3):

Should return 1. If the connection fails or the role is rejected, fix that before installing β€” the Hub will crash-loop otherwise.

S3 access (prereqs Step 4):

Both should succeed silently (exit code 0). This requires the AWS CLI with credentials configured on your workstation β€” it confirms the buckets exist and that some credentials can reach them, but doesn't validate the in-cluster IRSA path. If your workstation isn't AWS-configured, skip this check; Hub logs surface S3 misconfiguration immediately on install.

Step 3 β€” Create Kubernetes secrets

You'll need to create three Kubernetes secrets yourself:

  • Database credentials

  • The GitHub App private key (the PEM you saved during prereqs Step 5)

  • A signed Hub licence JWT (from your Earthly contact)

Other secrets, like the Hub auth token, GitHub webhook secret, and Grafana admin password β€” are auto-generated by the chart on first install and preserved across upgrades. You'll retrieve their values after install in Step 6.

Create the three with kubectl, or provision them with your secret manager of choice, using the same names and keys as specified below.

Database credentials:

GitHub App private key β€” base64-encode the PEM and store it as the private-key field:

The PEM must be base64-encoded inside the secret value β€” the Hub decodes it after reading the env var. (Kubernetes then base64-encodes the whole secret again for etcd storage, so it's double-encoded at rest.)

Hub licence JWT β€” store the signed token in hub-licence.jwt:

If your collectors need credentials at runtime (github.* collectors, Datadog, Jira, Linear, etc.), provision a fourth secret as well β€” see Script runtime secrets at the bottom of this page.

Step 4 β€” Write values.yaml

Create a values.yaml with the minimum configuration the chart needs. The example below is a working baseline β€” copy it, fill in the placeholders, and adjust for your environment. The GitHub App ID and installation ID come from prereqs Step 5.

Everything else has a sensible default. See the chart README for the full values reference.

A note on ingress

The Hub serves two protocols on separate ports β€” gRPC on 8000 (API) and plain HTTP on 8001 (webhook receiver + pre-signed URL redirector for run logs). Most ingress controllers apply backend-protocol per-Ingress rather than per-path, so the chart renders two Ingress resources sharing the same hostname and TLS config whenever hub.ingress.enabled: true. Per-protocol annotations layer over the shared annotations map: grpcAnnotations apply only to the gRPC Ingress, httpAnnotations only to the HTTP one. Both Ingresses are always created together β€” there's no per-protocol toggle.

If you'd rather front the Hub yourself (a controller with per-path backend protocols, a service mesh, or a LoadBalancer Service), set hub.ingress.enabled: false and route to the hub-grpc (port 8000) and hub-http (port 8001) service ports directly.

AWS credentials

The chart does not manage AWS credentials. The example above uses IRSA on EKS (annotation on the service account), which is the recommended pattern. The conceptual breakdown of patterns is in prereqs Step 4 β†’ Region and credentials.

Pulling images from GHCR

Lunar Hub images are published to a private GitHub Container Registry (ghcr.io/earthly/*). To pull them your cluster needs an image pull secret. You can derive the pull secret with the Lunar CLI:

If your snippet pods run in a separate namespace (operator.snippetNamespace), run it again for that namespace:

Reference the secret in your values.yaml:

Step 5 β€” Install

The chart fails fast with a clear error if any of the required fields are missing.

Watch the pods come up:

Expected rollout, in order:

  1. lunar-hub-<hash> starts as Running 0/1 while the container runs DB migrations, then flips to Running 1/1 once the readiness probe passes.

  2. lunar-operator-<hash> depends on some of the migrations applied by lunar-hub-<hash>. Once those have applied, it flips to Running 1/1 shortly after.

First-boot migrations can take a minute or two on a warm DB. If the Hub pod restarts repeatedly, check the logs:

Common first-install failures (visible while the Hub is starting up):

Symptom in logs
Fix

pq: permission denied for schema public

The DB role lacks CREATE on the database. Grant it, or use a database-owner role. See prerequisites step 3.

dial tcp ...: i/o timeout

The Hub pod can't reach Postgres on the network β€” security group, network policy, or firewall block between the pod and the DB. Check VPC routing and any NetworkPolicy in the lunar namespace.

dial tcp ...: connect: connection refused

Postgres is reachable but not listening on the configured port (hub.db.port, default 5432), or the process is down.

lookup ...: no such host

DNS issue β€” the DB hostname doesn't resolve from inside the cluster.

failed to parse private key

The lunar-github-app secret doesn't contain a valid PEM, or the PEM wasn't base64-encoded before going into the secret value (see the base64 note in Step 3). Recreate from the PEM you saved during prerequisites step 5.

The Hub validates Postgres at startup, but defers GitHub and S3 to first use β€” those misconfigurations won't appear in install logs. They surface once something exercises them (e.g. lunar hub pull github://... or a script run):

Symptom (after triggering an operation)
Likely cause

401 Unauthorized from GitHub API

Wrong hub.github.app.id, or the PEM in the lunar-github-app secret doesn't match the App GitHub knows about.

404 Not Found on installations/<id>/...

Wrong hub.github.app.installId, or the App is no longer installed on the org.

AccessDenied from S3

IAM policy too narrow β€” confirm s3:GetObject and s3:PutObject on both buckets. See prerequisites step 4.

NoSuchBucket from S3

Bucket name mismatch between hub.s3.logsBucket / hub.s3.resourcesBucket and what actually exists.

PermanentRedirect from S3

AWS_REGION doesn't match the bucket's region.

Step 6 β€” Verify the install

Once both pods are Running 1/1, confirm the Hub is healthy end to end.

Retrieve the Hub auth token β€” the chart generated this on first install. You'll need it for the CLI step below.

Retrieve the Grafana admin credentials β€” you'll need them to log in below:

The commands above assume the release name lunar (from helm install lunar ...). For other release names, see Required secrets in the chart README for the resolved name.

Grafana access β€” open https://grafana.lunar.example.com and log in with the credentials above. The home dashboard surfaces policy results, component health, and collection activity. If the page doesn't load, confirm the grafana.lunar.example.com A / CNAME record points at your ingress controller and the cert is valid.

HTTP ingress reachability β€” the webhook endpoint rejects unsigned requests but returns a predictable status, so it's a decent routing smoke test:

Expected: 400 (the Hub rejects the request for missing headers). 404 or 502 probably means the HTTP Ingress isn't reaching the Hub β€” confirm hub.ingress.enabled: true in your values, and that DNS still resolves to your ingress controller.

CLI connectivity smoke test β€” install the Lunar CLI, then save this minimal lunar-config.yml locally and run:

This authenticates over gRPC and returns the configured script runtime secrets (an empty list on a fresh install). Success means the CLI can reach the Hub through ingress, TLS terminates correctly, and the auth token is valid. Adjust the ports if you're using NodePort or non-default ingress.

End-to-end with a config repo β€” if you have a lunar-config.yml repository ready, pull it in:

This exercises the full path: CLI β†’ Hub β†’ GitHub (App auth) β†’ repo fetch β†’ Hub stores config. As a side effect, the Hub registers a webhook on every repo referenced in the config. Verify webhook registration by checking any of those repos β†’ Settings β†’ Webhooks in GitHub β€” you should see a new entry pointing at https://lunar.example.com/webhooks/github.

If you don't have a config repo yet, that's the natural next step β€” see the lunar-config docs for setup.

Script runtime secrets

Collectors or catalogers that hit external APIs (github.* collectors, Datadog, Jira, Linear, etc.) may need credentials at runtime. Each plugin's README in earthly/lunar-lib lists the LUNAR_SECRET_* variables it expects β€” drop the prefix when writing the secret. The Hub re-adds LUNAR_SECRET_ when it injects each key as an env var in the script pod, so GH_TOKEN in the secret surfaces as $LUNAR_SECRET_GH_TOKEN in the script.

Provision the secret with one --from-literal per variable:

Wire it into values.yaml (hub.secrets.*.perKey requires chart 2.2.0+). The same secret can back both scopes; omit the one you don't use:

See day-2 β†’ Other secrets for rotation.

Next steps

Last updated