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

Prerequisites

Before you run helm install lunar, the external dependencies below must be in place. The install walkthrough assumes you have them.

If you want a total picture of what you're about to deploy, read the overview first.

Lunar Hub is supported on Kubernetes only. Bare-metal and Docker installations are not supported.

Install the Lunar CLI first. The walkthrough uses it on your workstation to derive the cluster's GHCR image-pull secret from your licence — before helm install.

Before you begin

Make sure you have the following available before continuing.

Step
What you need
Detail

Workstation tools

lunar, kubectl, helm 3.x

Kubernetes cluster

1.29+, with an ingress controller and a default StorageClass

DNS hostnames + TLS certs

Two hostnames (Hub + Grafana), both reachable from GitHub (or your GHES instance) for the Hub, and from your users for Grafana

PostgreSQL instance

16+, where you can create a dedicated owner role

S3-compatible buckets

Two private buckets (logs + resources) with IAM to read and write them

GitHub org admin access

Or a personal account, where you can create a GitHub App for your org

Hub license key

Provided by Earthly; mounted by the chart as lunar-hub-licence

If you're standing up EKS from scratch, earthly/lunar-terraform-quickstart is a working reference module you can fork if desired.

Step 1 — Plan your Kubernetes namespaces

We recommend splitting the install across two namespaces:

  • Control-plane namespace (e.g. lunar). This is the release namespace. It hosts the Lunar Hub, Operator, and Grafana. These are the parts that need to stay up, and will be updated by Helm.

  • Run Pods namespace (e.g. lunar-scripts). This hosts the short-lived pods the operator spawns to execute cataloger, collector, and policy batches. We recommend this separate namespace because:

    • this workload is ephemeral, and can be rather "bursty" in number and resource requirements. You can tune resources and limits independently here.

    • this code is user-supplied (e.g. your plugins, scripts, third-party catalogers), not Lunar's. This is a different trust boundary, where you can tighten RBAC, egress, and resource limits independently.

Both namespaces must exist before helm install. The chart will not create the run-pods namespace for you. Point the operator at it with operator.snippetNamespace.

Single-namespace installs also work — leave operator.snippetNamespace unset and everything runs in the release namespace. This is fine for trying things out, or small setups; but is not recommended for production configurations.

Step 2 — Check your Kubernetes cluster

Requirement
Detail

Kubernetes

1.29 or newer.

Helm

3.x.

StorageClass

A StorageClass must be available — your cluster default is probably fine. The chart provisions a 10 GiB ReadWriteOnce PVC for Hub state. You can tune details if needed via hub.persistence.* (chart README).

Ingress controller

Must support gRPC backend routing. See Ingress in the chart README for an NGINX-tested example.

DNS

Two hostnames pointing at your ingress controller's external IP — one for the Hub (e.g. lunar.example.com) and one for Grafana (e.g. grafana.lunar.example.com). GitHub, CI integrations, and Lunar CLI users must reach the Hub hostname; the Grafana hostname is for your team's browser access.

TLS certificate

The Hub and Grafana pods both listen plaintext. Terminate TLS at your ingress or an upstream load balancer for each hostname.

Step 3 — Provision PostgreSQL

Lunar needs a single PostgreSQL database. The Hub runs migrations on every startup and manages several of its own schemas (including the default public). We recommend that you give it a dedicated DB where its role is the owner.

Requirement
Detail

Version

PostgreSQL 16 or newer.

Connectivity

Reachable from both the Hub and Operator pod's network. The chart does not include Postgres.

Role

Dedicated DB role for the Hub. Minimum: database owner (to create schemas, and grants on created objects) plus cluster-level CREATEROLE (to create a read-only sqlapi_user role during migration). SUPERUSER also works.

Extensions

Optional: pg_stat_statements enabled in shared_preload_libraries. The Hub's diagnostics bundle uses it when present. Setting shared_preload_libraries requires a Postgres restart (or parameter-group reboot on RDS / Cloud SQL), so it's easier to enable at provisioning time than later.

Connection pool

The Hub runs as a single replica today and opens up to 40 connections by default. Size your Postgres (or PgBouncer) accordingly. See Scaling in Day 2 Operations for tuning context.

SSL

The Hub negotiates TLS by default via hub.db.connectionOptions: "sslmode=require". Most managed Postgres (RDS, Aurora, Cloud SQL) ships with TLS forced and will connect out of the box. Plain Postgres deployments without TLS must set hub.db.connectionOptions: "sslmode=disable" explicitly. Format is libpq KV pairs, space-separated — to pass extra options write "sslmode=require connect_timeout=10" (NOT &-separated URL query). The default is not merged in when overridden, so include sslmode= yourself.

Backups

Use your existing Postgres backup process; backups are your responsibility. All authoritative Hub and Operator state lives entirely in Postgres.

Shared Postgres cluster? Because CREATEROLE is a cluster-level attribute, Postgres has no mechanism to scope it to a single database. If granting it cluster-wide is too broad, pre-create the sqlapi_user role yourself (any password) and leave HUB_SQLAPI_PASSWORD unset. The migration's IF NOT EXISTS check skips role creation, leaving only schema-level grants — which the Hub's role can do as database owner.

Step 4 — Provision S3-compatible object storage

Lunar needs two private S3 buckets, both writable by the Hub:

  • A Logs bucket, which contains per-run log files. These can be short-lived; a 30-day lifecycle rule is reasonable here.

  • A Resources bucket, which contains run-bundle archives fetched by init containers. Keep these as long as you might re-run historical catalogers, collectors, or policies.

Both buckets must block public access. Content may include credentials, user script source code, or PII surfaced from CI runs. The Hub serves all reads via time-limited pre-signed URLs.

The Hub only calls PutObject, GetObject, and HeadObject on both buckets, and issues pre-signed GET/PUT URLs for both. Minimum IAM policy on AWS:

Region and credentials

The Hub picks up AWS credentials via the standard SDK credential chain — the chart stays out of the credentials business. Region must be set explicitly:

Common patterns (see the chart README for full YAML):

  • EKS. Annotate the chart's service account with an IAM role for service accounts.

  • Static credentials. Inject AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from a secret via hub.extraEnv. Not recommended for production.

  • IMDS, pod identity, external secrets operators. All the usual AWS-SDK-friendly mechanisms work.

Non-AWS backends

MinIO, Cloudflare R2, and GCS in S3-compatibility mode all work — set AWS_ENDPOINT_URL_S3 via hub.extraEnv:

Lunar uses virtual-host-style S3 addressing only. Most MinIO, R2, and GCS deployments handle this out of the box. Path-style-only backends aren't currently supported.

Step 5 — Create a GitHub App

The Hub authenticates to GitHub as a GitHub App. Use our hosted setup tool to create one in a couple of clicks.

Need to create the App manually? If your GitHub instance is air-gapped, you're using GitHub Enterprise Server, or your security review needs every permission and event laid out before the App is created, see manual GitHub App setup instead.

Using the setup tool

  1. Follow the prompts. The tool uses GitHub's manifest flow to register the App with the right permissions and events.

  2. Download the PEM private key when prompted. GitHub shows it exactly once — if you click past this page, you'll have to generate a new key from the App settings later.

  3. Click Install App on GitHub and select the org. Choose All repositories unless you have a specific reason not to — Lunar's actual monitoring scope is configured in lunar-config.yml, so a narrower scope here just means coming back to Org Settings → GitHub Apps → Lunar → Repository access every time you add a new repo to Lunar.

The hosted tool proxies the manifest exchange to GitHub and returns the credentials to your browser; we never persist them.

Capture these before continuing

When you finish Step 5, you'll have four things — confirm you've saved all four before moving on:

What
Source

Owner (the GitHub org or user the App is installed on, e.g. earthly)

The <org> in the install URL below

App ID (numeric, e.g. 3635822)

Setup tool result page

Installation ID (numeric)

URL after install: https://github.com/organizations/<org>/settings/installations/<INSTALL_ID> — the trailing number

PEM private key

Downloaded from the setup tool — shown only once, save it now

Set them via HUB_GITHUB_APP_OWNER, HUB_GITHUB_APP_ID, HUB_GITHUB_APP_INSTALL_ID, and HUB_GITHUB_APP_PRIVATE_KEY (or the chart equivalents under hub.github.app.*). All four are required in single-App mode; the Hub refuses to start without them.

For GitHub Enterprise Server, you also need to set hub.github.baseUrl in your values to point at your GHES instance.

Installing in multiple GitHub orgs with one Hub

When the Hub fronts multiple customer orgs that each install their own Lunar App, register one entry per owner in HUB_GITHUB_APPS:

Each entry pins its own App credentials and installation; the Hub mints tokens scoped to the right org per request. Owner matching is case-insensitive and trimmed.

For the PEM files, the recommended pattern is one Kubernetes Secret with multiple keys, mounted as a single volume:

HUB_GITHUB_APPS is mutually exclusive with the single-App env vars (HUB_GITHUB_APP_OWNER / HUB_GITHUB_APP_ID / HUB_GITHUB_APP_PRIVATE_KEY / HUB_GITHUB_APP_INSTALL_ID). Use one mode or the other.

Step 6 — Plan your Kubernetes secrets

Seven Kubernetes secrets come into play at install time. You create four; the chart generates the other three (with helm.sh/resource-policy: keep, so they survive upgrades and uninstalls).

Secret
Who creates it
Contents

lunar-db

You

DB username and password

lunar-github-app

You

private-key — the PEM from Step 5

lunar-hub-licence

You

hub-licence.jwt — Hub licence token

regcred

You (derived from licence)

GHCR image-pull credentials — generated by lunar licence pull-secret from your licence JWT

<release>-auth-token

Chart (auto-generated)

Shared bearer token for the CLI and CI agents

<release>-github-webhook

Chart (auto-generated)

Per-repo webhook signing secret — the Hub registers this with GitHub automatically when it creates per-repo webhooks

<release>-grafana-admin

Chart (auto-generated)

Grafana admin username and password

GitOps alternative. If your setup needs deterministic secret management, you can pre-create any chart-managed secret and point the chart at it (e.g. hub.github.webhookSecret.secretName). See the chart README for the full list of *.secretName values you can override.

The install walkthrough has the exact kubectl create secret commands for the three user-created secrets.

Step 7 — Size for capacity

The chart sets no default resource requests or limits. The numbers below are reasonable starting points, but you should monitor and adjust them based on your specific needs.

Component
CPU request
Memory request

Hub

500m

1 Gi

Operator

100m

128 Mi

Run pods are short-lived batch pods spawned by the operator. Each pod contains N user containers (one per script in the batch) plus an init container and a sidecar. Per-user-container resources come from operator.snippetContainerSpec* — the operator's built-in defaults request 250m / 256 Mi for collectors and catalogers, 50m / 128 Mi for policies.

Batch size is per script type via operator.batchMaxCount* (defaults: 10 for collectors and catalogers, 20 for policies — policies pack denser because each container is lighter). Concurrent batch pods are capped by operator.maxConcurrent (default 10), shared across script types.

Hub workers that feed run pods can be capped with HUB_MAX_WORKERS_COLLECT, HUB_MAX_WORKERS_POLICY, HUB_MAX_WORKERS_CRON_COLLECT, and HUB_MAX_WORKERS_CATALOGER; 0 means unlimited. Tune these when increasing operator.maxConcurrent. The Hub and operator have separate Postgres pool caps for operator work via HUB_MAX_OPERATOR_POOL_SIZE and OPERATOR_MAX_POOL_SIZE.

The run-pods namespace needs headroom for:

At defaults that's ~25 GiB of concurrent-run memory per script type (10 pods × 10 collectors × 256 Mi, or 10 × 20 × 128 Mi for policies).

If you want sizing guidance once you're past initial install, please reach out.

Next steps

When your prerequisites are in place:

Last updated