> For the complete documentation index, see [llms.txt](https://docs-lunar.earthly.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-lunar.earthly.dev/install/lunar-hub/self-hosted/install-walkthrough.md).

# Install Walkthrough

This walkthrough takes you from zero to a working Lunar Hub. It assumes you've worked through the [prerequisites](/install/lunar-hub/self-hosted/prerequisites.md), 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

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

Verify:

```bash
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](/install/lunar-hub/self-hosted/prerequisites.md). An issue here is cheaper to find now than after you've created secrets and written your `values.yaml`.

**DNS** ([prereqs Step 2](/install/lunar-hub/self-hosted/prerequisites.md#step-2-check-your-kubernetes-cluster)):

```bash
nslookup lunar.example.com
```

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](/install/lunar-hub/self-hosted/prerequisites.md#step-3-provision-postgresql)):

```bash
kubectl run -it --rm --restart=Never -n lunar pg-check \
  --image=postgres:16 -- \
  psql "postgresql://<user>:<pass>@<host>:5432/lunar" \
  -c "SELECT 1;"
```

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](/install/lunar-hub/self-hosted/prerequisites.md#step-4-provision-s3-compatible-object-storage)):

```bash
aws s3api head-bucket --bucket your-lunar-logs-bucket
aws s3api head-bucket --bucket your-lunar-resources-bucket
```

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
* Your Git platform credential: the GitHub App PEM, or the GitLab group access token, from [prereqs Step 5](/install/lunar-hub/self-hosted/prerequisites.md#step-5-connect-your-git-platform)
* A signed Hub licence JWT (from your Earthly contact)

Other secrets, like the Hub auth token, the webhook signing secrets, and the 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](#step-6-verify-the-install).

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:**

```bash
kubectl -n lunar create secret generic lunar-db \
  --from-literal=username='<db-user>' \
  --from-literal=password='<db-password>'
```

**Git platform credential** — create the one that matches your platform, or both if you run both:

{% tabs %}
{% tab title="GitHub" %}
Base64-encode the App's PEM and store it as the `private-key` field:

```bash
kubectl -n lunar create secret generic lunar-github-app \
  --from-literal=private-key="$(base64 < path/to/lunar-github-app-<id>.pem | tr -d '\n')"
```

{% hint style="info" %}
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.)
{% endhint %}
{% endtab %}

{% tab title="GitLab" %}
The Hub reads the group access token from a mounted file, so it goes in as plain text with no base64 step:

```bash
kubectl -n lunar create secret generic lunar-gitlab-token \
  --from-literal=token='<your-group-access-token>'
```

Step 4 mounts this secret at the `token_path` you declare in `HUB_GITLAB_TOKENS`.
{% endtab %}
{% endtabs %}

**Hub licence JWT** — store the signed token in `hub-licence.jwt`:

```bash
kubectl -n lunar create secret generic lunar-hub-licence \
  --from-literal=hub-licence.jwt='<signed-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](#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. Its `hub.github` block assumes GitHub; [Git platform credentials](#git-platform-credentials) below covers what to change on GitLab.

```yaml
# Cluster assumptions baked into this sample:
#   - NGINX ingress controller    → hub.ingress.className + grpcAnnotations
#   - cert-manager for TLS        → ingress annotations (drop if BYO secret)
#   - EKS IRSA for S3 credentials → serviceAccount.annotations (see "AWS credentials" below)

# IRSA on EKS: annotate the service account with your IAM role ARN.
# See "AWS credentials" below for non-EKS options.
serviceAccount:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/lunar-hub-s3

hub:
  publicBaseURL: "https://lunar.example.com"
  licence:
    secretName: "lunar-hub-licence"
    secretKey: "hub-licence.jwt"

  # Run the Hub as multiple replicas. The Hub is stateless — all state lives
  # in Postgres and S3 — so replicas coordinate through Postgres (leader
  # election, advisory locks) with no peer-to-peer wiring. See "Replicas and
  # the Postgres connection budget" below before raising this.
  replicaCount: 3
  # Keep a quorum during node drains and rollouts. Enable this whenever
  # replicaCount > 1 (a PDB on a single replica blocks voluntary node drains).
  podDisruptionBudget:
    enabled: true
    maxUnavailable: 1

  # Pin all four image tags (hub, operator, initImage, sidecarImage) to the
  # same released hub version for a reproducible install. The latest is
  # published to ghcr.io/earthly/lunar-hub — see the repo's `lunar-hub-v*`
  # release tags. This example is bumped automatically on each Hub release.
  image:
    tag: "3.12.0"

  db:
    host: "your-db-host.example.com"
    name: "lunar"

  s3:
    logsBucket: "your-lunar-logs-bucket"
    resourcesBucket: "your-lunar-resources-bucket"

  github:
    app:
      # Don't forget the quotes, otherwise they might get rendered as scientific notation!
      id: "123456"
      installId: "78901234"
      # The GitHub org (or user) the App is installed on. Required since chart 2.2.0.
      owner: "<your-github-org>"

  # AWS region for S3. Credentials themselves come from the AWS SDK
  # credential chain (IRSA above, or see "AWS credentials" below).
  extraEnv:
    - name: AWS_REGION
      value: us-east-1

  # The chart sets no resource defaults. Numbers below are the starting
  # recommendations from prereqs Step 7 — tune as you observe real load.
  resources:
    requests:
      cpu: 500m
      memory: 1Gi

  ingress:
    enabled: true
    host: lunar.example.com
    className: nginx
    tls:
      - secretName: lunar-tls
        hosts: [lunar.example.com]
    annotations:
      # cert-manager provisions the `lunar-tls` secret automatically.
      # Remove this annotation if you supply the TLS secret yourself
      # (corporate CA, external LB, ACM, etc.).
      cert-manager.io/cluster-issuer: letsencrypt
    # gRPC backend-protocol applies only to the gRPC Ingress. Both
    # Ingresses are always rendered when ingress.enabled is true.
    grpcAnnotations:
      nginx.ingress.kubernetes.io/backend-protocol: "GRPC"

operator:
  # Run pods are created here. The namespace must already exist; the chart
  # will not create it. See prereqs Step 1 for the trust-boundary rationale.
  snippetNamespace: "lunar-scripts"

  image:
    tag: "3.12.0"
  initImage:
    tag: "3.12.0"
  sidecarImage:
    tag: "3.12.0"

  resources:
    requests:
      cpu: 100m
      memory: 128Mi

# Grafana — the primary UI for Lunar. Enabled by default: the chart runs a
# stock Grafana and installs Lunar's dashboards into it. This block just wires
# up its ingress. If you'd rather use a Grafana you already run (Cloud,
# Enterprise, or self-hosted OSS), drop this block and see the "Grafana"
# section below for how to point Lunar at it instead.
grafana:
  ingress:
    enabled: true
    hosts: [grafana.lunar.example.com]
    tls:
      - secretName: lunar-grafana-tls
        hosts: [grafana.lunar.example.com]
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt
```

Everything else has a sensible default. See the [chart README](https://github.com/earthly/charts/blob/main/README.md#values-reference) for the full values reference.

### Git platform credentials

Serving both platforms from one Hub is supported: configure both, and each repository is routed by its host.

{% tabs %}
{% tab title="GitHub" %}
The `hub.github.app` block in the sample above is the whole configuration. Its `id`, `installId`, and `owner` come from [prereqs Step 5](/install/lunar-hub/self-hosted/prerequisites.md#step-5-connect-your-git-platform); the private key comes from the `lunar-github-app` secret created in Step 3. All four are required together; a partial App configuration is rejected at startup.

On **GitHub Enterprise Server**, also point the Hub at your instance:

```yaml
hub:
  github:
    baseUrl: https://github.example.com
```

Fronting several GitHub orgs from one Hub takes a different shape. See [GitHub → multiple organizations](/install/git-platforms/github.md#multiple-organizations).
{% endtab %}

{% tab title="GitLab" %}
The chart has dedicated values for the GitHub App (`hub.github.app.*`) but not yet for GitLab, so GitLab credentials go in through the generic passthroughs: the environment variable via `hub.extraEnv`, and the token file via a mounted secret.

```yaml
hub:
  extraEnv:
    # ... alongside AWS_REGION from the sample above
    - name: HUB_GITLAB_TOKENS
      value: '[{"group":"acme","host":"gitlab.com","token_path":"/secrets/gitlab/token"}]'

  volumeMounts:
    - name: gitlab-token
      mountPath: /secrets/gitlab
      readOnly: true
  volumes:
    - name: gitlab-token
      secret:
        secretName: lunar-gitlab-token
```

One entry per top-level group; `token_path` must match the `mountPath` above. The webhook signing secret is not in there: the chart generates it, the same as it does for GitHub. [GitLab → Hub configuration](/install/git-platforms/gitlab.md#hub-configuration-self-hosted) has the full field reference and the multi-group form.

{% hint style="warning" %}
**Keep the `hub.github` block even on a GitLab-only install.** The Hub itself needs only one Git platform configured, but the chart does not yet know that: its template guard requires the GitHub App values and aborts the render with `hub.github.app.id is required (numeric, non-zero)` when they are absent. Until the chart gains dedicated GitLab values, a chart install has to carry a GitHub App configuration alongside `HUB_GITLAB_TOKENS`.
{% endhint %}
{% endtab %}
{% endtabs %}

### Replicas and the Postgres connection budget

The Hub is stateless, so `replicaCount` scales it horizontally with no extra wiring — but each replica opens its own pool of Postgres connections, so total connections scale roughly linearly with the replica count:

```
total ≈ replicaCount × (hub.db.maxOpenConns + hub.db.maxPoolConns + hub.db.operatorPoolSize)
```

The chart defaults (`40 + 40 + 5 ≈ 85` per replica) match the Hub's built-in values, so a single replica is unchanged. Before running several replicas, confirm the total stays within your Postgres `max_connections` — leave headroom for the migrate Job, `psql`, and superuser-reserved slots. For larger replica counts, **lower `hub.db.maxOpenConns` / `hub.db.maxPoolConns`** so the total fits, and/or front Postgres with a connection pooler. Keep `maxPoolConns` at or above your peak concurrent queue workers (the sum of `hub.maxWorkers.*`) — the pool is shared by every queue, so a pool below that serializes store queries under load.

The chart soft-spreads replicas across nodes by default (`topologySpreadConstraints`, `maxSkew 1` across `kubernetes.io/hostname`, `ScheduleAnyway`) so a single node loss doesn't take out the whole Hub. Override `hub.topologySpreadConstraints` to impose your own (e.g. a hard zone spread) — it replaces the default wholesale.

### 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.

### Grafana

Lunar ships dashboards for policy results, component health, and collection activity, and installs them — with their datasources and panel plugins — into a Grafana over its HTTP API. There are three ways to provide that Grafana; pick the one that matches what you already run. The `grafana:` block in the values above shows the first (default) option.

In every case the chart auto-generates the read-only `grafana_user` datasource password (`<release>-grafana-db`) and wires it end-to-end for you. If you'd rather provide your own secret, set `grafana.provisioning.dbPassword.secretName` (recommended for GitOps).

#### Option A — bundled Grafana (default)

The default (`grafana.mode: chart`): the chart deploys a stock Grafana next to the Hub, runs a post-install/upgrade Job that installs the dashboards into it, and auto-generates the admin login (which the Hub uses to reach the pod).

Wire up the ingress in the `grafana:` block above so your team can reach it in a browser, and retrieve the admin login in [Step 6](#step-6-verify-the-install). Nothing else is required — `grafana.mode` defaults to `chart`.

#### Option B — Grafana Cloud or Grafana Enterprise (service-account token)

Point the Hub at your existing Grafana with a [service-account token](https://grafana.com/docs/grafana/latest/administration/service-accounts/). Create a service account with the **Admin** role, generate a token, then disable the bundled Grafana and wire the token in:

```yaml
grafana:
  mode: external                  # bring your own Grafana (no bundled pod)
  url: "https://your-org.grafana.net"
  auth:
    secretName: lunar-grafana     # holds the service-account token (created below)
    tokenKey: token
```

That's the whole configuration. On `helm install` / `helm upgrade` the chart's `post-install` / `post-upgrade` hook Job runs the provisioning tool against your Grafana — `deploy.sh` resolves the URL, credentials, and read-only datasource connection from the Hub over gRPC, then installs the plugins, datasources, and dashboards over Grafana's HTTP API. The Job is idempotent and non-blocking (a failed provision is logged but never rolls back the Hub upgrade), and it re-runs on every subsequent upgrade so the dashboards stay in sync with the Hub's schema. No manual step.

#### Option C — self-hosted OSS Grafana (admin username + password)

Same as Option B, but OSS Grafana can't install plugins with a service-account token — the dashboards' panel plugins need a **server admin**. Give the Hub an admin username and password instead of a token:

```yaml
grafana:
  mode: external
  url: "https://grafana.example.com"
  auth:
    secretName: lunar-grafana     # holds username + password (created below)
    # userKey / passwordKey default to "username" / "password"
```

Deploy the same way as Option B: `helm install` / `helm upgrade` runs the chart's hook Job automatically.

The examples reference a `lunar-grafana` secret you create — the key depends on your option: `token` for Option B, or `username` + `password` for Option C:

```bash
# Option B (Cloud / Enterprise)
kubectl -n lunar create secret generic lunar-grafana \
  --from-literal=token='<grafana-service-account-token>'

# Option C (OSS) — use these keys instead of `token`
kubectl -n lunar create secret generic lunar-grafana \
  --from-literal=username='admin' \
  --from-literal=password='<grafana-admin-password>'
```

{% hint style="info" %}
`grafana.url` + `grafana.auth` are all the chart needs for an external Grafana — it wires them into the Hub for you (as `HUB_GRAFANA_URL_BASE` and `HUB_GRAFANA_TOKEN`, or `HUB_GRAFANA_USER` / `HUB_GRAFANA_PASSWORD`), so there's no `hub.extraEnv` plumbing. The chart's provisioning Job runs whenever `grafana.mode != off` — the same Job that serves the bundled pod — installing the dashboards on every install/upgrade.
{% endhint %}

{% hint style="warning" %}
Options B and C need three network paths:

* **your Grafana → the Hub's Postgres** (as the read-only `grafana_user` role) — the Postgres datasource
* **your Grafana → the Hub's HTTP API** — the Infinity datasource
* **the in-cluster Job → your Grafana** (HTTP) — the deploy Job

Same-network self-hosted Grafana has all three. Grafana Cloud needs a route to both Hub endpoints (public endpoint, PrivateLink, or Private Data Source Connect) so the in-cluster Job can reach it.
{% endhint %}

### 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](/install/lunar-hub/self-hosted/prerequisites.md#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](/install/cli.md):

```bash
lunar licence pull-secret \
  --licence-file=path/to/hub-licence.jwt \
  --namespace=lunar \
  --name=regcred | kubectl apply -f -
```

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

```bash
lunar licence pull-secret \
  --licence-file=path/to/hub-licence.jwt \
  --namespace=lunar-scripts \
  --name=regcred | kubectl apply -f -
```

Reference the secret in your `values.yaml`:

```yaml
imagePullSecrets:
  - name: regcred

hub:
  image:
    repository: ghcr.io/earthly/lunar-hub

operator:
  image:
    repository: ghcr.io/earthly/lunar-snippet-operator
  initImage:
    repository: ghcr.io/earthly/lunar-snippet-init
  sidecarImage:
    repository: ghcr.io/earthly/lunar-snippet-sidecar
```

Lunar also ships the Grafana dashboards in a private deploy image, `ghcr.io/earthly/lunar-dashboards`, which the chart's dashboards-deploy Job pulls under the same `imagePullSecrets` — it uses that repository by default, so no extra image configuration is needed.

## Step 5 — Install

```bash
helm install lunar earthly/lunar \
  --namespace lunar \
  -f values.yaml
```

The chart fails fast with a clear error if any of the [required fields](https://github.com/earthly/charts/blob/main/README.md#required-values) are missing.

Watch the pods come up:

```bash
kubectl -n lunar get pods -w
```

Expected rollout, in order:

1. `lunar-hub-migrate-<hash>` runs first — a pre-install Helm hook Job that applies DB migrations and gates the rest of the rollout. It runs to `Completed` before any Hub pod is created.
2. `lunar-hub-<hash>` pods start (one per `replicaCount`). Each asserts the schema is current at boot and refuses to start if it's behind — so serving pods never run against an un-migrated schema. They flip to `Running 1/1` once the readiness probe (`/ready`) passes.
3. `lunar-operator-<hash>` depends on the applied migrations. Once the Hub is up, it flips to `Running 1/1` shortly after.

Migrations can take a minute or two on a warm DB. If the migrate Job fails, or a Hub pod restarts repeatedly, check the logs:

```bash
# Migrate Job (if it failed the hook, the whole install/upgrade is rolled back):
kubectl -n lunar logs -l app.kubernetes.io/component=hub-migrate --tail=200
# Hub pods:
kubectl -n lunar logs -l app.kubernetes.io/component=hub --tail=200
```

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](/install/lunar-hub/self-hosted/prerequisites.md#step-3-provision-postgresql).                                                                                                                                                          |
| `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](#step-3-create-kubernetes-secrets)). Recreate from the PEM you saved during [prerequisites step 5](/install/lunar-hub/self-hosted/prerequisites.md#step-5-connect-your-git-platform). |
| `no forge configured`                       | Neither GitHub App credentials nor `HUB_GITLAB_TOKENS` are set. At least one Git platform is required. See [prerequisites step 5](/install/lunar-hub/self-hosted/prerequisites.md#step-5-connect-your-git-platform).                                                                                                                                  |
| `read HUB_GITLAB_TOKENS ... token_path`     | The GitLab token file isn't where the config says it is. Confirm `token_path` matches the `mountPath` of the volume you mounted the `lunar-gitlab-token` secret at.                                                                                                                                                                                   |

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](/install/lunar-hub/self-hosted/prerequisites.md#step-4-provision-s3-compatible-object-storage). |
| `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 the Hub and Operator 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.

```bash
kubectl -n lunar get secret lunar-auth-token \
  -o jsonpath='{.data.token}' | base64 -d
```

**Grafana access** — this depends on which [Grafana option](#grafana) you chose.

If you use the **bundled Grafana** (Option A), retrieve its auto-generated admin credentials:

```bash
kubectl -n lunar get secret lunar-grafana-admin \
  -o jsonpath='{.data.username}' | base64 -d
  
kubectl -n lunar get secret lunar-grafana-admin \
  -o jsonpath='{.data.password}' | base64 -d
```

The commands above assume the release name `lunar` (from `helm install lunar ...`). For other release names, see [Required secrets](https://github.com/earthly/charts/blob/main/README.md#required-secrets) in the chart README for the resolved name.

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.

If you pointed Lunar at **your own Grafana** (Option B or C), log in there with your existing credentials — the Lunar dashboards appear in a "Lunar" folder.

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

{% tabs %}
{% tab title="GitHub" %}

```bash
curl -sS -o /dev/null -w "%{http_code}\n" https://lunar.example.com/webhooks/github
```

{% endtab %}

{% tab title="GitLab" %}

```bash
curl -sS -o /dev/null -w "%{http_code}\n" https://lunar.example.com/webhooks/gitlab
```

{% endtab %}
{% endtabs %}

Expected: `405`. The endpoints accept only `POST`, and the method check runs before any signature validation, so a plain `GET` like this is rejected with `405 Method Not Allowed`. That still proves the route reaches the Hub. `404` or `502` probably means the HTTP Ingress isn't reaching the Hub, so 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](/install/cli.md), then save this minimal [`lunar-config.yml`](/configuration/lunar-config.md) locally and run:

```yaml
version: 0
hub:
  host: lunar.example.com
  grpcPort: 443
  httpPort: 443
collectors: []
policies: []
```

```bash
export LUNAR_HUB_TOKEN=<token-from-step-above>
lunar secret list
```

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`](/configuration/lunar-config.md) repository ready, pull it in. This exercises the full path: CLI → Hub → your Git platform → repo fetch → Hub stores config. As a side effect, the Hub registers a webhook on every repository referenced in the config.

{% tabs %}
{% tab title="GitHub" %}

```bash
lunar hub pull github://your-org/your-config-repo@main
```

Verify the webhook on any repository in the config: repo → **Settings → Webhooks**, pointing at `https://lunar.example.com/webhooks/github`.
{% endtab %}

{% tab title="GitLab" %}

```bash
lunar hub pull gitlab://gitlab.com/your-group/your-config-repo@main
```

The host is required in the URL, even for gitlab.com.

Verify the webhook on any project in the config: project → **Settings → Webhooks**, pointing at `https://lunar.example.com/webhooks/gitlab`.
{% endtab %}
{% endtabs %}

If you don't have a config repo yet, that's the natural next step — see [the lunar-config docs](/configuration/lunar-config.md) 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`](https://github.com/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:

```bash
kubectl -n lunar create secret generic lunar-script-secrets \
  --from-literal=GH_TOKEN='<your-pat>' \
  --from-literal=LINEAR_API_KEY='<your-linear-key>'
  # ... one --from-literal per LUNAR_SECRET_* variable your plugins expect
```

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:

```yaml
hub:
  secrets:
    collector:
      secretName: lunar-script-secrets
      perKey: true
    cataloger:
      secretName: lunar-script-secrets
      perKey: true
```

See [day-2 → Other secrets](/install/lunar-hub/self-hosted/day-2-operations.md#other-secrets) for rotation.

## Next steps

* [Install the Lunar CLI](/install/cli.md) on your workstation.
* [Install the Lunar CI Tracer](/install/ci-tracer/github-actions-self-hosted.md) on your CI runners.
* [Day-2 operations](/install/lunar-hub/self-hosted/day-2-operations.md) — upgrades, secret rotation, observability, uninstall.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs-lunar.earthly.dev/install/lunar-hub/self-hosted/install-walkthrough.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
