> 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-ci-agent/agent-self-hosted.md).

# Self-Hosted Runners

The Lunar CI Agent instruments CI/CD pipelines to collect metadata during builds, tests, scans, and deployments. It wraps your existing runner process, monitors execution, and triggers scripts at the right moments.

This page covers the most common setup: **self-hosted runners** (including GitHub Actions self-hosted runners). If you're using GitHub-hosted managed runners, see [Managed Runners](/install/lunar-ci-agent/agent-managed.md).

## Prerequisites

You need an existing self-hosted runner infrastructure (e.g. GitHub Actions self-hosted runner). Lunar adds instrumentation on top of your existing setup — it does not manage the runner lifecycle.

## Adding to an Existing Runner

{% stepper %}
{% step %}

## Download and install the Lunar CLI

<a href="https://github.com/earthly/lunar-dist/releases/latest" class="button primary" data-icon="download">Download the Lunar CLI</a>

Or via the command line:

```bash
curl -LO https://github.com/earthly/lunar-dist/releases/download/v2.5.0/lunar-linux-amd64
chmod +x lunar-linux-amd64 && sudo mv lunar-linux-amd64 /usr/local/bin/lunar
```

{% hint style="info" %}
Replace the version above with the latest from the [releases page](https://github.com/earthly/lunar-dist/releases/latest).
{% endhint %}

The CI agent is no longer installed separately. The CLI fetches it on first use through your Lunar Hub (see the last step), so the only binary you install here is `lunar`.
{% endstep %}

{% step %}

## Set the required environment variables

```bash
export LUNAR_CI_TYPE=github
export LUNAR_HUB_TOKEN=your_hub_token
export LUNAR_HUB_HOST=your_hub_host
export LUNAR_HUB_GRPC_PORT=your_grpc_port
export LUNAR_HUB_HTTP_PORT=your_http_port
export LUNAR_RUN_CMD=path_to_github_runner_run.sh
```

The agent auto-detects state, cache, and bundle directories based on the running user:

* **Root** → system paths (`/var/lib/lunar`, `/var/cache/lunar/git-repos`, `/var/tmp/lunar/...`).
* **Non-root** → user paths under `$HOME/.lunar/`.

Override any of them by setting `LUNAR_STATE_DIR`, `LUNAR_GIT_CACHE_DIR`, `LUNAR_BUNDLE_DIR`, `LUNAR_SNIPPET_DIR`, `LUNAR_SCRIPT_LOG_DIR`, `LUNAR_BIN_DIR`, or `LUNAR_LOCK_DIR` — useful for read-only rootfs runners, systemd units with `ProtectHome=`, or non-standard home directories.
{% endstep %}

{% step %}

## Run the agent

```bash
lunar ci-tracer run
```

On first run, `lunar ci-tracer run` downloads the agent through your Hub, verifies it, caches it under `LUNAR_BIN_DIR` (`$HOME/.lunar/bin` by default), and then runs it. Subsequent runs reuse the cached binary and skip the download.

{% hint style="info" %}
The Hub must be reachable the first time you run this on a host (or whenever the cache is empty). Once the agent binary is cached, the agent only needs the Hub for its normal runtime traffic.
{% endhint %}

{% hint style="info" %}
**Upgrading from an earlier agent install?** Previous versions shipped a separate `lunar-ci-agent` binary that you downloaded and started yourself. That separate download no longer exists — install only the `lunar` CLI and start the agent with `lunar ci-tracer run`, which fetches the agent for you.
{% endhint %}

{% hint style="info" %}
For production usage, run `lunar ci-tracer run` under a process supervisor such as `systemd` so it restarts automatically on failure. See [Systemd Configuration](/install/lunar-ci-agent/systemd.md) for an example unit file.
{% endhint %}
{% endstep %}
{% endstepper %}

## Using a Custom Runner Image

If your runners are containerized, install the `lunar` CLI inside a `Dockerfile` and make `lunar ci-tracer run` the entrypoint. The agent binary is fetched on first boot through your Hub — the build host does not need Hub connectivity:

{% code title="Dockerfile" %}

```dockerfile
FROM my-custom-runner-image:ubuntu-slim

ENV LUNAR_HUB_HOST=my.cool.host.com
ENV LUNAR_HUB_GRPC_PORT=443
ENV LUNAR_HUB_HTTP_PORT=443
ENV LUNAR_CI_TYPE=github
ENV LUNAR_RUN_CMD=/home/ubuntu/actions-runner/run.sh

# Replace /home/ubuntu with your runner user's home directory
ENV LUNAR_STATE_DIR=/home/ubuntu/.lunar/state
ENV LUNAR_GIT_CACHE_DIR=/home/ubuntu/.lunar/git-repos
ENV LUNAR_BUNDLE_DIR=/home/ubuntu/.lunar/bundles
ENV LUNAR_SNIPPET_DIR=/home/ubuntu/.lunar/snippets
ENV LUNAR_SCRIPT_LOG_DIR=/home/ubuntu/.lunar/scripts
ENV LUNAR_BIN_DIR=/home/ubuntu/.lunar/bin
ENV LUNAR_LOCK_DIR=/home/ubuntu/.lunar/lock

RUN curl -LO https://github.com/earthly/lunar-dist/releases/download/v2.5.0/lunar-linux-amd64 && \
    chmod +x lunar-linux-amd64 && mv lunar-linux-amd64 /usr/local/bin/lunar

ENTRYPOINT ["lunar", "ci-tracer", "run"]
```

{% endcode %}

On the first container boot, `lunar ci-tracer run` accesses your Lunar Hub to download and cache the agent under `LUNAR_BIN_DIR`, then runs it. Subsequent boots reuse the cached binary **only if `LUNAR_BIN_DIR` is persisted** across container restarts (e.g. a mounted volume) — otherwise each fresh container downloads the agent again on first boot.

{% hint style="info" %}
**Optional: warm the cache at build time.** Run `lunar ci-tracer install` during the build to download and cache the agent without starting it. This requires the build host to reach the Hub. Pass the Hub token as a build secret so it never lands in an image layer:

```dockerfile
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=lunar_hub_token \
    LUNAR_HUB_TOKEN="$(cat /run/secrets/lunar_hub_token)" \
    lunar ci-tracer install
```

```bash
docker build --secret id=lunar_hub_token,env=LUNAR_HUB_TOKEN .
```

The agent is cached in `LUNAR_BIN_DIR`, so it's baked into the image layer. Make sure `LUNAR_BIN_DIR` points at a path that stays in the image.
{% endhint %}

{% hint style="info" %}
The GitHub Actions runner may not work correctly when run as root. See GitHub's [self-hosted runner documentation](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners) for details.
{% endhint %}

Bake everything into the image except secrets. Pass the Hub token at runtime:

```bash
docker run -e LUNAR_HUB_TOKEN=$(vault read /lunar/hub/token) runner-image:latest
```

Or in Kubernetes, reference a Secret:

```yaml
env:
  - name: LUNAR_HUB_TOKEN
    valueFrom:
      secretKeyRef:
        name: lunar-hub
        key: token
```

For all environment variable details, see the [Configuration Reference](/install/lunar-ci-agent/agent-config.md).

***

## Running `sudo` and setuid binaries in traced workflows

To trace command execution, the agent installs a seccomp filter on the runner process. Linux only permits installing that filter when the process either holds the `CAP_SYS_ADMIN` capability or has the `no_new_privs` flag set. When the runner does **not** have `CAP_SYS_ADMIN`, the agent falls back to setting `no_new_privs`, which makes the kernel ignore the setuid bit. Non-root processes can then no longer escalate through setuid-root binaries — `sudo`, for example, refuses to run:

```
sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
```

`no_new_privs` is inherited by every child process and can never be cleared, so this affects any unprivileged user your workflow switches to, not just the top-level runner user.

To keep `sudo` and other setuid binaries working under tracing, run the runner **as root** in a container that adds `CAP_SYS_ADMIN`. The agent detects the capability on the runner process and skips `no_new_privs`, installing the seccomp filter through the capability instead.

{% code title="docker" %}

```bash
docker run --cap-add SYS_ADMIN runner-image:latest
```

{% endcode %}

{% code title="kubernetes" %}

```yaml
securityContext:
  capabilities:
    add: ["SYS_ADMIN"]
```

{% endcode %}

{% hint style="info" %}
The runner must run **as root** for this to take effect: a capability added to a container is only held by a root (uid 0) process. A non-root runner process does not gain `CAP_SYS_ADMIN` from the container, so the agent still falls back to `no_new_privs` there.
{% endhint %}

{% hint style="warning" %}
`CAP_SYS_ADMIN` is a broad, privileged capability — grant it only when your workflows genuinely need setuid escalation (e.g. `sudo`) under tracing. On [GitHub-hosted managed runners](/install/lunar-ci-agent/agent-managed.md) the job runs as the non-root `runner` user with no way to hold `CAP_SYS_ADMIN` — its passwordless `sudo` is a sudoers grant, not a capability — so setuid escalation is not supported while tracing there.
{% endhint %}

***

## Next Steps

Once installed, you can begin configuring:

* [Collectors](/configuration/lunar-config/collectors.md) to gather SDLC data
* [Policies](/configuration/lunar-config/policies.md) to enforce standards
* [Domains and Components](/docs/key-concepts.md) to organize your software landscape

For questions or enterprise onboarding:

<a href="https://earthly.dev/earthly-lunar/demo" class="button secondary" data-icon="envelope">Contact the Earthly team</a>


---

# 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-ci-agent/agent-self-hosted.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.
