Skip to content
329 linesCodeBlameRaw
1---
2title: Self-hosted runners
3description: Run your workflow jobs, and if you choose your agents' work, on your own machines. Their time costs nothing.
4---
5
6A self-hosted runner is a machine of yours that runs your workflow jobs
7instead of g1t's sandboxes. It can be a server, a Mac in a cupboard, a
8Windows box with a GPU, or a pod in your cluster. You install `g1t-runner`
9on it, register it once, and it picks up jobs that ask for it with
10`runs-on: self-hosted`.
11
12- **Time on your runners is $0**, on every plan, including the free one.
13 It shows on [usage](/guides/usage-and-billing/) as self-hosted minutes.
14- **It only connects out.** The runner polls `api.g1t.sh` over HTTPS for
15 work. Nothing needs to reach the machine: no open port, no tunnel.
16- **Any OS.** Linux, macOS and Windows, on x64 and arm64. Jobs run in a
17 Docker container by default, or directly on the machine.
18
19| Where | Page | Who manages it |
20| --- | --- | --- |
21| A workspace | **Settings → Runners**, `g1t.sh/<workspace>/-/runners` | Owners only. |
22| A project | **Settings → Runners**, `g1t.sh/<workspace>/<project>/settings/runners` | People with the Admin [role](/guides/access-and-roles/) on its repository |
23
24A workspace's runners serve the repositories their [group](#groups) allows.
25A project's own runners serve only that project.
26
27## Add a runner
28
291. Open **Settings → Runners** and click **New runner**. g1t makes a
30 registration token and shows the commands for Linux, macOS, Windows and
31 Docker with it filled in. The token lasts an hour, can register any
32 number of runners until then, and is shown once.
332. On the machine, download `g1t-runner` and register it:
34
35 ```sh
36 # Linux (x64; use g1t-runner-linux-arm64 on arm64)
37 curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-linux-x64
38 chmod +x g1t-runner
39 ./g1t-runner register --url https://g1t.sh --token g1trt_…
40 ```
41
42 ```sh
43 # macOS (Apple silicon; use g1t-runner-macos-x64 on Intel)
44 curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-macos-arm64
45 chmod +x g1t-runner
46 ./g1t-runner register --url https://g1t.sh --token g1trt_…
47 ```
48
49 ```powershell
50 # Windows (PowerShell)
51 Invoke-WebRequest https://g1t.sh/downloads/runner/latest/g1t-runner-windows-x64.exe -OutFile g1t-runner.exe
52 .\g1t-runner.exe register --url https://g1t.sh --token g1trt_…
53 ```
54
553. Start it: `./g1t-runner run` keeps it running in the terminal, or
56 install it as a service that starts with the machine:
57
58 | OS | Command | What it makes |
59 | --- | --- | --- |
60 | Linux | `sudo ./g1t-runner service install` | A systemd unit, `g1t-runner-<name>`; its log is `journalctl -u g1t-runner-<name>` |
61 | macOS | `./g1t-runner service install` | A launch agent, `sh.g1t.g1t-runner-<name>`; its log is `~/.g1t-runner/runner.log` |
62 | Windows | `.\g1t-runner.exe service install`, from an Administrator prompt | A Windows service, `g1t-runner-<name>`, that restarts if it stops |
63
64 `service start`, `stop`, `status` and `uninstall` do what they say.
65
66The runner shows up in the list within a few seconds, with a green dot.
67
68### Register options
69
70| Option | |
71| --- | --- |
72| `--url` | The g1t you register with: `https://g1t.sh`. |
73| `--token` | The registration token. |
74| `--name` | What to call it. The machine's name if left out. Names are unique within a workspace (or project). |
75| `--labels` | Extra labels, comma-separated: `gpu,cuda-12`. See [labels](#labels). |
76| `--group` | A workspace runner's [group](#groups). The default group, or the one the token was made for, if left out. |
77| `--ephemeral` | Take one job, then remove itself. For [autoscaling](#autoscaling). |
78| `--no-docker` | Run jobs directly on the machine instead of in containers. |
79| `--image` | The image workflow jobs run in when they name no `container:`. `node:24-bookworm` if left out. |
80| `--agent-image` | The image [agent work](#agents-on-your-runners) runs in. |
81| `--work-dir` | Where jobs run with `--no-docker` keep their files. `~/.g1t-runner/work` if left out. |
82| `--dir` | Where the runner keeps its configuration and credential. `~/.g1t-runner`, or `G1T_RUNNER_DIR`, if left out. Every command takes it. |
83| `--replace` | Take the place of a runner with the same name. |
84| `--no-auto-update` | Never update itself. |
85
86To run several runners on one machine, give each its own `--dir` and
87`--name`.
88
89## Use it in a workflow
90
91A job runs on a self-hosted runner when its `runs-on` names `self-hosted`:
92
93```yaml
94jobs:
95 test:
96 runs-on: self-hosted
97 steps:
98 - uses: actions/checkout@v5
99 - run: make test
100
101 gpu:
102 runs-on: [self-hosted, linux, gpu]
103 steps:
104 - run: nvidia-smi
105
106 windows:
107 runs-on: [self-hosted, windows]
108 steps:
109 - run: Get-ComputerInfo | Select-Object OsName
110```
111
112Until a runner with every label the job names takes it, the job waits, and
113its page says what for: *Waiting for a self-hosted runner with labels
114self-hosted, linux, gpu.* A job that waits a day fails. Jobs that have
115waited ten minutes with no matching runner online show under **Needs you**
116on Mission control.
117
118`runs-on: { group: GPU, labels: [linux] }` asks for a runner in the group
119called GPU as well.
120
121### Labels
122
123Every runner has `self-hosted`, its OS (`linux`, `macos` or `windows`) and
124its architecture (`x64` or `arm64`), then the labels you gave it. A runner
125that runs jobs in Docker is `linux`, whatever the machine is, since that is
126what its jobs run on. Labels are matched without regard to case.
127
128A job takes a runner only when every label in its `runs-on` is one of the
129runner's. Labels that name g1t's own machines, such as `ubuntu-latest` or
130`g1t-4core`, never go to a self-hosted runner.
131
132### What a job gets
133
134A job on your runner runs exactly as it would in g1t's sandbox: the same
135harness, the same `${{ }}` contexts, `GITHUB_*` variables, secrets and
136workflow commands, the same log and artifacts. `runner.name`, `runner.os`
137and `runner.arch` are the machine's, and `RUNNER_ENVIRONMENT` is
138`self-hosted`.
139
140- **In Docker** (the default), each job gets a fresh container from its
141 `container:` image or the runner's `--image`, removed when it ends. The
142 runner needs Docker, and its user needs to be allowed to use it. Its
143 `services:` are not started, since the job's container has no Docker of
144 its own; the log says so.
145- **With `--no-docker`**, each job gets a fresh folder under the work
146 folder, removed when it ends, and runs with whatever the machine has
147 installed. A step's default shell is `bash` on Linux and macOS and
148 PowerShell on Windows; `shell: pwsh`, `powershell`, `cmd`, `bash` and
149 `python` work where installed. On a machine with Docker, the job's
150 `services:`, `container:`, `docker://` steps and Docker actions use
151 the machine's Docker, as on GitHub's runners.
152
153A self-hosted job stops at 60 minutes unless its `timeout-minutes` says
154more, up to 24 hours (1440). Jobs on g1t's own machines stop at 60 minutes.
155
156## Groups
157
158A workspace's runners are in groups, which say which repositories may use
159them. Every workspace has a **Default** group, for every repository, which
160runners join unless their token or `--group` names another.
161
162To keep a set of machines for some repositories, open **Settings →
163Runners**, click **New group**, name it, and choose **Only these**
164repositories. Then click **New runner** with that group chosen, or register
165with `--group`. Deleting a group moves its runners to the default group.
166
167## Agents on your runners
168
169**Settings → Runners → Where work runs** can send g1t's own work to your
170runners too: agent runs, checks, reviews, merge checks and the merge queue.
171Switch on **Run g1t's work on self-hosted runners** and give the labels
172a runner needs to take it (`self-hosted` is always one).
173
174- The agent works exactly as in g1t's sandbox: the same harness, with a
175 short-lived credential that can do only what that kind of run may, in
176 that repository, revoked when it ends.
177- Its model calls still go through g1t's model proxy, `models.g1t.sh`, with
178 that credential, so budgets, caps and the audit log work as before. With
179 your own [model provider](/guides/models/), g1t charges nothing for the
180 run; otherwise the model is paid as usual and the machine is free.
181- Agent work needs an image with git, Node and the agent's CLI: register
182 the runner with `--agent-image` and an image of yours that has them, or
183 run it with `--no-docker` on a Linux machine set aside for it, where
184 `/work` can be written. g1t does not publish an agent image yet.
185- [Guardrails](/guides/guardrails/) still apply to what the agent does, but
186 their network list cannot be enforced on your machine. The run's session
187 says so when it starts.
188
189A project can have its own setting, or follow the workspace's.
190
191## Billing
192
193Time on your runners is free. Each job's minutes go on
194[usage](/guides/usage-and-billing/) as **Self-hosted runner time** at $0,
195so you can see how much ran there.
196
197- Workflow jobs on your runners need no plan and no card: they work on the
198 free tier.
199- Agent work on your runners still needs its model paid for: the g1t plan,
200 the trial, the open-source pool on a public repository, or your own
201 model provider, which makes the run free.
202
203## Security
204
205- **Pull requests from forks never run on your runners** unless you allow
206 it under **Settings → Runners → Where work runs**. Leave it off on a
207 public repository: anyone who can open a pull request could run any code
208 on the machine, read what it can reach, and leave something behind for
209 the next job. Such jobs get no secrets either way.
210- **Secrets** reach a job on your runner under the same rules as on g1t's:
211 only trusted runs get them, and only the ones for the job's environment.
212- **The runner's credential** can poll for work, report on what it was
213 given and remove itself, and nothing else; every other endpoint refuses
214 it. It is kept only as a hash on g1t's side, rotates every day, and stops
215 working the moment the runner is removed. It is never given to a job:
216 each job gets only its own short-lived token.
217- **Isolation**: a job in Docker gets a container of its own, removed after.
218 A job with `--no-docker` runs as the runner's user, in a folder of its
219 own; it can reach whatever that user can. Use a dedicated account, and
220 ephemeral runners for untrusted code.
221- **Network**: g1t's network guardrails are not enforced on your machines.
222 A job reaches whatever the machine can.
223- **Audit**: making a registration token, registering, removing, and every
224 job a runner takes are in the [audit log](/guides/audit-log/), with the
225 runner as the actor.
226- Only owners (or a repository's admins) register and remove runners,
227 signed in or with a person's token with `runners:admin`. Workspace
228 tokens, `G1T_TOKEN` included, cannot, so a workflow cannot add a machine
229 to run its own jobs.
230
231## Docker and Kubernetes
232
233The runner's image runs `register-and-run`, which registers once and then
234runs. Each option can also come from `G1T_RUNNER_<OPTION>` in the
235environment, such as `G1T_RUNNER_TOKEN` and `G1T_RUNNER_LABELS`, so a
236secret can hold the token:
237
238```sh
239docker run -d --name g1t-runner --restart unless-stopped \
240 -v /var/run/docker.sock:/var/run/docker.sock \
241 -v g1t-runner:/data -e G1T_RUNNER_DIR=/data \
242 g1t.sh/flagon-io/g1t-runner register-and-run --url https://g1t.sh --token g1trt_…
243```
244
245With the host's Docker socket, each job runs in a sibling container.
246
247### Autoscaling
248
249Ephemeral runners take one job and remove themselves, so each job starts on
250a clean machine. In Kubernetes, run them with `--no-docker` in pods that
251make their own registration token from an owner's personal
252[access token](/guides/authentication/#access-tokens) with only
253`runners:admin`, and let the Deployment replace each pod that finishes:
254
255```yaml
256apiVersion: apps/v1
257kind: Deployment
258metadata:
259 name: g1t-runner
260spec:
261 replicas: 3
262 selector: { matchLabels: { app: g1t-runner } }
263 template:
264 metadata: { labels: { app: g1t-runner } }
265 spec:
266 containers:
267 - name: runner
268 image: g1t.sh/flagon-io/g1t-runner
269 command: ["sh", "-c"]
270 args:
271 - |
272 export G1T_RUNNER_TOKEN="$(curl -fsS -X POST \
273 -H "Authorization: Bearer $G1T_ADMIN_TOKEN" \
274 https://api.g1t.sh/workspaces/acme/actions/runners/registration-token \
275 | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
276 exec g1t-runner register-and-run
277 env:
278 - { name: G1T_RUNNER_URL, value: https://g1t.sh }
279 - { name: G1T_RUNNER_LABELS, value: k8s }
280 - { name: G1T_RUNNER_EPHEMERAL, value: "1" }
281 - { name: G1T_RUNNER_NO_DOCKER, value: "1" }
282 - name: G1T_ADMIN_TOKEN
283 valueFrom: { secretKeyRef: { name: g1t, key: runners-admin-token } }
284```
285
286To scale with demand rather than keep a fixed number, change `replicas`
287with your autoscaler of choice: [`list_runners`](/reference/api/runners/list-runners-for-workspace/)
288says which are busy.
289
290## Updates
291
292The runner checks for a new release every six hours while it is idle, and
293updates itself when the release is signed by g1t's release key and its
294download matches the release's SHA-256. `g1t-runner update` does it now;
295`--no-auto-update` turns it off. Releases are at
296`https://g1t.sh/downloads/runner/<version>/`, with a `SHA256SUMS` file.
297
298## Remove a runner
299
300On the machine, `g1t-runner remove` unregisters it and forgets its
301credential (run `service uninstall` first if it is a service). Or remove it
302from **Settings → Runners**; the runner stops on its next poll. A job it was
303running fails. Runners offline for 14 days are removed by themselves.
304
305## API and MCP
306
307| What | REST | MCP (`workflow` tool) | Scope |
308| --- | --- | --- | --- |
309| List runners | [`GET /workspaces/{workspace}/actions/runners`](/reference/api/runners/list-runners-for-workspace/), [`GET /repos/{owner}/{name}/actions/runners`](/reference/api/runners/list-runners/) | `list_runners` | `runners:read` |
310| Make a registration token | [`POST …/actions/runners/registration-token`](/reference/api/runners/create-runner-registration-token-for-workspace/) | `create_runner_token` | `runners:admin` |
311| Remove a runner | [`DELETE …/actions/runners/{id}`](/reference/api/runners/remove-runner-for-workspace/) | `remove_runner` | `runners:admin` |
312| Groups | [`GET`](/reference/api/runners/list-runner-groups/), [`POST`](/reference/api/runners/create-runner-group/), [`PATCH`](/reference/api/runners/update-runner-group/), [`DELETE`](/reference/api/runners/delete-runner-group/) `/workspaces/{workspace}/actions/runner-groups` | `list_runner_groups`, `create_runner_group`, `update_runner_group`, `delete_runner_group` | `runners:read`, `runners:admin` |
313| Where work runs | [`GET`](/reference/api/runners/get-runner-settings-for-workspace/), [`PATCH`](/reference/api/runners/update-runner-settings-for-workspace/) `…/actions/runner-settings` | `get_runner_settings`, `update_runner_settings` | `runners:read`, `runners:admin` |
314
315The [Agent preset](/guides/authentication/#scopes) does not include
316`runners:read`.
317
318## Troubleshooting
319
320| What you see | What to do |
321| --- | --- |
322| The job says *Waiting for a self-hosted runner with labels …* | No online runner has every one of those labels in a group that allows the repository. Compare the labels on **Settings → Runners**, check the runner's group, or start the runner. |
323| *Pull requests from forks do not run on self-hosted runners here.* | The run is from a fork. Allow it under **Where work runs**, if you trust everyone who can open a pull request. |
324| `register` says the token is not valid | It expired after an hour, or was mistyped. Make a new one. |
325| `register` says a runner with that name is registered | Choose another `--name`, or add `--replace`. |
326| `run` says *g1t no longer knows this runner* | It was removed, or its credential was. Register it again. |
327| *could not run docker* | Install Docker and start it, give the runner's user access to it, or register with `--no-docker`. |
328| Agent work fails at once | Give the runner `--agent-image`, or run it on Linux with `--no-docker`. |
329| The runner is offline in the list | It has not polled for 90 seconds. Check its log (`journalctl -u g1t-runner-<name>`, `~/.g1t-runner/runner.log`, or the terminal) and that the machine can reach `https://api.g1t.sh`. |