Read-only mirror. Development happens at git.bnema.dev; issues and pull requests are closed here.
ermite-runner fetches jobs from an ermite server and runs workflows written for GitHub Actions (.github/workflows/*.yml). Each job runs in its own sandbox, destroyed when the job ends.
Isolation modes
| Mode |
Sandbox |
Needs |
vm |
A Firecracker microVM per job |
A usable /dev/kvm |
container |
A container per job with its own Docker daemon |
A rootful Docker Engine |
ERMITE_RUNNER_ISOLATION=auto is the default. It picks vm when /dev/kvm is usable and Firecracker, the guest kernel and the base image exist. Otherwise it picks container. The chosen mode and the reasons are logged at start.
Details: docs/sandboxes.md.
Deployment options
| Option |
Runs as |
Isolation |
Needs |
| Unprivileged container (docs/container.md) |
An unprivileged user (rootless Podman or Docker) |
vm, no jailer |
/dev/kvm, /dev/net/tun and CAP_NET_ADMIN in the container |
| Host service (below) |
root |
vm with the jailer, or container |
See the requirements below |
The container image bundles the runner, Firecracker, the guest kernel and the base image. It is the simplest way to run vm mode without root on the host.
Requirements (host service)
- A Linux host. The runner runs as root.
ip, nft and mkfs.ext4 (e2fsprogs 1.43 or newer).
- Go, to build the runner.
- For
vm: /dev/kvm, firecracker and jailer in PATH, a guest kernel and a base image.
- For
container: a rootful Docker Engine and the job image. sysbox is optional: without it the runner uses a privileged container.
docs/images.md explains how to build the kernel and the images.
Build
The binary must be static:
CGO_ENABLED=0 go build -trimpath -o ermite-runner .
Register and run (host service)
In a container, the runner registers itself on its first start instead: see docs/container.md.
-
On the ermite server, create a registration token:
ermite token create my-runner --preset runner
-
Install the binary, the unit and the settings:
sudo install -m 0755 ermite-runner /usr/local/bin/ermite-runner
sudo install -m 0644 contrib/ermite-runner.service /etc/systemd/system/
sudo install -d -m 0700 /etc/ermite-runner
sudo install -m 0600 /dev/stdin /etc/ermite-runner/env <<'ENV'
ERMITE_RUNNER_URL=https://ermite.example.com
ERMITE_RUNNER_NAME=runner-1
ERMITE_RUNNER_LABELS=ubuntu-latest,ubuntu-26.04
ERMITE_RUNNER_LOG_FORMAT=json
ENV
-
Register once. The token goes through standard input, so it is in no command line:
printf %s "$TOKEN" | sudo systemd-run --pipe --wait \
-p EnvironmentFile=/etc/ermite-runner/env \
-p StateDirectory=ermite-runner -p StateDirectoryMode=0700 \
/usr/local/bin/ermite-runner register --token-stdin
-
Start the service:
sudo systemctl enable --now ermite-runner
journalctl -u ermite-runner -f
ermite-runner run polls the server for jobs and runs each one in a sandbox, up to ERMITE_RUNNER_CAPACITY jobs at a time. On SIGTERM it cancels the running jobs, reports them and removes their sandboxes.
To check an installation without a server, run ermite-runner sandbox-check. It runs one stub script in the selected mode.
More on the service, logs and shutdown: docs/service.md.
Commands
| Command |
Purpose |
ermite-runner register [--token-stdin] [--token T] [--replace] |
Register with the server and store the runner token |
ermite-runner run |
Fetch and run jobs |
ermite-runner sandbox-check [script] |
Run one stub script in a sandbox |
ermite-runner version |
Print the version |
Key settings
| Variable |
Default |
Meaning |
ERMITE_RUNNER_URL |
|
Base URL of the ermite server. https://, or http:// with ERMITE_RUNNER_INSECURE_HTTP=1 |
ERMITE_RUNNER_NAME |
host name |
Runner name |
ERMITE_RUNNER_LABELS |
|
Extra labels, comma separated |
ERMITE_RUNNER_ISOLATION |
auto |
auto, vm or container |
ERMITE_RUNNER_CAPACITY |
1 |
Jobs in parallel |
ERMITE_RUNNER_JOB_CPUS |
2 |
CPUs per job |
ERMITE_RUNNER_JOB_MEMORY |
4G |
Memory per job |
ERMITE_RUNNER_JOB_DISK |
64G |
Scratch disk per job |
ERMITE_RUNNER_STATE_DIR |
/var/lib/ermite-runner |
State directory |
All settings: docs/configuration.md.
Security notes
- Run only one runner per host.
- The registration token can read the code and secrets of every repository. Pass it through standard input.
- Without sysbox,
container mode uses a privileged container, which is equivalent to host root. Set ERMITE_RUNNER_DENY_PRIVILEGED=1 to refuse it.
- As a host service, the state directory and all its parents must be owned by root and not writable by others.
- In an unprivileged container, the jailer is off: the container and its user namespace confine Firecracker instead, and each job still runs in its own microVM.
Documentation
License
MIT. See LICENSE. Third-party attributions: THIRD_PARTY.md.
source
1<h1 align="center">ermite-runner</h1>
2
3<p align="center">
4 <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License: MIT"></a>
5 <a href="https://github.com/bnema/ermite-runner/commits/main"><img src="https://badgen.net/github/last-commit/bnema/ermite-runner/main?icon=github" alt="Last commit"></a>
6 <a href="https://github.com/bnema/ermite-runner/stargazers"><img src="https://badgen.net/github/stars/bnema/ermite-runner?icon=github" alt="GitHub stars"></a>
7</p>
8
9<p align="center">The CI runner of <a href="https://github.com/bnema/ermite">ermite</a>: runs GitHub Actions workflows, one microVM per job.</p>
10
11> Read-only mirror. Development happens at git.bnema.dev; issues and pull requests are closed here.
12
13---
14
15ermite-runner fetches jobs from an ermite server and runs workflows written for GitHub Actions (`.github/workflows/*.yml`). Each job runs in its own sandbox, destroyed when the job ends.
16
17## Isolation modes
18
19| Mode | Sandbox | Needs |
20| --- | --- | --- |
21| `vm` | A Firecracker microVM per job | A usable `/dev/kvm` |
22| `container` | A container per job with its own Docker daemon | A rootful Docker Engine |
23
24`ERMITE_RUNNER_ISOLATION=auto` is the default. It picks `vm` when `/dev/kvm` is usable and Firecracker, the guest kernel and the base image exist. Otherwise it picks `container`. The chosen mode and the reasons are logged at start.
25
26Details: [docs/sandboxes.md](docs/sandboxes.md).
27
28## Deployment options
29
30| Option | Runs as | Isolation | Needs |
31| --- | --- | --- | --- |
32| Unprivileged container ([docs/container.md](docs/container.md)) | An unprivileged user (rootless Podman or Docker) | `vm`, no jailer | `/dev/kvm`, `/dev/net/tun` and `CAP_NET_ADMIN` in the container |
33| Host service (below) | root | `vm` with the jailer, or `container` | See the requirements below |
34
35The container image bundles the runner, Firecracker, the guest kernel and the base image. It is the simplest way to run `vm` mode without root on the host.
36
37## Requirements (host service)
38
39- A Linux host. The runner runs as root.
40- `ip`, `nft` and `mkfs.ext4` (e2fsprogs 1.43 or newer).
41- Go, to build the runner.
42- For `vm`: `/dev/kvm`, `firecracker` and `jailer` in `PATH`, a guest kernel and a base image.
43- For `container`: a rootful Docker Engine and the job image. [sysbox](https://github.com/nestybox/sysbox) is optional: without it the runner uses a privileged container.
44
45[docs/images.md](docs/images.md) explains how to build the kernel and the images.
46
47## Build
48
49The binary must be static:
50
51```sh
52CGO_ENABLED=0 go build -trimpath -o ermite-runner .
53```
54
55## Register and run (host service)
56
57In a container, the runner registers itself on its first start instead: see [docs/container.md](docs/container.md#first-start).
58
591. On the ermite server, create a registration token:
60
61 ```sh
62 ermite token create my-runner --preset runner
63 ```
64
652. Install the binary, the unit and the settings:
66
67 ```sh
68 sudo install -m 0755 ermite-runner /usr/local/bin/ermite-runner
69 sudo install -m 0644 contrib/ermite-runner.service /etc/systemd/system/
70 sudo install -d -m 0700 /etc/ermite-runner
71 sudo install -m 0600 /dev/stdin /etc/ermite-runner/env <<'ENV'
72 ERMITE_RUNNER_URL=https://ermite.example.com
73 ERMITE_RUNNER_NAME=runner-1
74 ERMITE_RUNNER_LABELS=ubuntu-latest,ubuntu-26.04
75 ERMITE_RUNNER_LOG_FORMAT=json
76 ENV
77 ```
78
793. Register once. The token goes through standard input, so it is in no command line:
80
81 ```sh
82 printf %s "$TOKEN" | sudo systemd-run --pipe --wait \
83 -p EnvironmentFile=/etc/ermite-runner/env \
84 -p StateDirectory=ermite-runner -p StateDirectoryMode=0700 \
85 /usr/local/bin/ermite-runner register --token-stdin
86 ```
87
884. Start the service:
89
90 ```sh
91 sudo systemctl enable --now ermite-runner
92 journalctl -u ermite-runner -f
93 ```
94
95`ermite-runner run` polls the server for jobs and runs each one in a sandbox, up to `ERMITE_RUNNER_CAPACITY` jobs at a time. On SIGTERM it cancels the running jobs, reports them and removes their sandboxes.
96
97To check an installation without a server, run `ermite-runner sandbox-check`. It runs one stub script in the selected mode.
98
99More on the service, logs and shutdown: [docs/service.md](docs/service.md).
100
101## Commands
102
103| Command | Purpose |
104| --- | --- |
105| `ermite-runner register [--token-stdin] [--token T] [--replace]` | Register with the server and store the runner token |
106| `ermite-runner run` | Fetch and run jobs |
107| `ermite-runner sandbox-check [script]` | Run one stub script in a sandbox |
108| `ermite-runner version` | Print the version |
109
110## Key settings
111
112| Variable | Default | Meaning |
113| --- | --- | --- |
114| `ERMITE_RUNNER_URL` | | Base URL of the ermite server. `https://`, or `http://` with `ERMITE_RUNNER_INSECURE_HTTP=1` |
115| `ERMITE_RUNNER_NAME` | host name | Runner name |
116| `ERMITE_RUNNER_LABELS` | | Extra labels, comma separated |
117| `ERMITE_RUNNER_ISOLATION` | `auto` | `auto`, `vm` or `container` |
118| `ERMITE_RUNNER_CAPACITY` | `1` | Jobs in parallel |
119| `ERMITE_RUNNER_JOB_CPUS` | `2` | CPUs per job |
120| `ERMITE_RUNNER_JOB_MEMORY` | `4G` | Memory per job |
121| `ERMITE_RUNNER_JOB_DISK` | `64G` | Scratch disk per job |
122| `ERMITE_RUNNER_STATE_DIR` | `/var/lib/ermite-runner` | State directory |
123
124All settings: [docs/configuration.md](docs/configuration.md).
125
126## Security notes
127
128- Run only one runner per host.
129- The registration token can read the code and secrets of every repository. Pass it through standard input.
130- Without sysbox, `container` mode uses a privileged container, which is equivalent to host root. Set `ERMITE_RUNNER_DENY_PRIVILEGED=1` to refuse it.
131- As a host service, the state directory and all its parents must be owned by root and not writable by others.
132- In an unprivileged container, the jailer is off: the container and its user namespace confine Firecracker instead, and each job still runs in its own microVM.
133
134## Documentation
135
136- [Configuration](docs/configuration.md): every environment variable
137- [Sandboxes](docs/sandboxes.md): `vm` and `container` modes, network policy, cleanup
138- [Unprivileged container](docs/container.md): run the runner in a rootless container
139- [Service](docs/service.md): systemd unit, logs, shutdown
140- [Images](docs/images.md): job image, guest kernel
141- [Workflows](docs/workflows.md): job and service containers, cancellation, annotations
142- [Cache and artifacts](docs/cache-and-artifacts.md): Actions cache and artifacts v4
143- [Node.js externals](docs/externals.md): Node.js for JavaScript actions
144- [Development](docs/development.md): build, tests
145
146## License
147
148MIT. See [LICENSE](LICENSE). Third-party attributions: [THIRD_PARTY.md](THIRD_PARTY.md).