Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

827 linesCodeBlameRaw
1---
2title: Self-hosted runners
3description: Run your workflow jobs, and if you choose g1t's checks and merge queue, on your own machines. Their time costs nothing.
4---
5
6import { TabItem, Tabs } from '@astrojs/starlight/components';
7
8A self-hosted runner is a machine of yours that runs your workflow jobs
9instead of g1t's sandboxes. It can be a server, a Mac in a cupboard, a
10Windows box with a GPU, or a pod in your cluster. On Linux you run
11`g1t-runner` as a container under [Sysbox](#run-it-as-a-container), so
12neither the runner nor its jobs can reach the machine; it registers once
13and picks up jobs that ask for it with `runs-on: self-hosted`.
14
15- **Time on your runners is $0**, on every plan, including the free one.
16 It shows on [usage](/guides/usage-and-billing/) as self-hosted minutes.
17- **It only connects out.** The runner polls `api.g1t.sh` over HTTPS for
18 work. Nothing needs to reach the machine: no open port, no tunnel.
19- **Each job in a fresh sandbox.** Every job gets a container of its own,
20 made for it and removed after it, inside the runner's own container.
21 See [isolation](#isolation).
22- **Any OS.** Linux, macOS and Windows, on x64 and arm64. On your own
23 computer, `g1t-runner sandbox up` runs Linux jobs in a Linux VM of the
24 runner's own and Windows jobs each in a Windows Sandbox: see
25 [sandbox on your computer](#sandbox-on-your-computer).
26
27| Where | Page | Who manages it |
28| --- | --- | --- |
29| A workspace | **Workspace → Runners** in the rail, `g1t.sh/<workspace>/-/runners` | Owners only. |
30| A project | **Settings → Runners**, `g1t.sh/<workspace>/<project>/settings/runners` | People with the Admin [role](/guides/access-and-roles/) on its repository |
31
32A workspace's runners serve the repositories their [group](#groups) allows.
33A project's own runners serve only that project.
34
35## Run it as a container
36
37On a Linux machine, run the runner's image with the Sysbox runtime. The
38runner starts a Docker Engine of its own inside its container and runs
39each job in a container on that engine, so jobs are containers inside the
40runner's container. Sysbox gives the runner's container a user namespace
41of its own, so its root is an unprivileged user on the machine, and lets
42it run Docker without `--privileged` and without the machine's Docker
43socket.
44
451. Install [Sysbox](https://github.com/nestybox/sysbox) 0.7 or later on
46 the machine, following its install guide. Earlier versions cannot run
47 the runner's own Docker Engine, whose `runc` checks `/proc` in a way
48 they do not allow. Sysbox needs Linux; on a Mac or Windows computer,
49 use [the sandbox](#sandbox-on-your-computer) instead.
502. Make a registration token: open **Runners** and click **New runner**.
513. Run the runner:
52
53 ```sh
54 docker run -d --name g1t-runner --restart unless-stopped \
55 --runtime=sysbox-runc \
56 -v g1t-runner:/data \
57 -e G1T_RUNNER_URL=https://g1t.sh \
58 -e G1T_RUNNER_TOKEN=g1trt_… \
59 g1t.sh/flagon-io/g1t-runner
60 ```
61
62The image runs `register-and-run`, which registers once and then runs.
63Its configuration and credential are kept on the `g1t-runner` volume, so
64the container can be restarted and replaced without registering again.
65Every [register option](#register-options) can come from
66`G1T_RUNNER_<OPTION>` in the environment, such as `G1T_RUNNER_LABELS=gpu`
67or `G1T_RUNNER_MEMORY=8g`, so a secret can hold the token.
68
69Its log says which isolation it has:
70
71```text
72… Isolation: nested-container: each job runs in a fresh container on this runner's own Docker Engine, inside the runner's sandboxed container; neither reaches the machine.
73```
74
75Do not give the container the machine's Docker socket
76(`-v /var/run/docker.sock:/var/run/docker.sock`) or `--privileged`. The
77runner looks for both when it starts, and refuses to start if it finds
78them:
79
80| It finds | What it says |
81| --- | --- |
82| An engine's socket mounted in from the machine (`docker.sock`, `containerd.sock`, `podman.sock`, under any name) | *… is a container engine's socket mounted in from outside this container* |
83| An engine's socket it did not make, such as from a mounted `/var/run` | *… is a container engine's socket that this runner did not make* |
84| `DOCKER_HOST` set | *DOCKER_HOST is set …* |
85| Root in the container is root on the machine (no Sysbox, or `--privileged`) | *root in this container is root on the machine* |
86
87To start anyway, with jobs able to reach the machine, set
88`G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1`. The runner then logs each
89problem with `UNSAFE:` in front, and registers as `container` rather than
90`nested-container`.
91
92The engine's images and build cache are kept in the container, so they
93last until the container is removed. Sysbox keeps them on the machine's
94disk for that container alone; to keep them when you replace the
95container, add `-v g1t-runner-docker:/var/lib/docker`, a volume for one
96runner container at a time.
97
98## Isolation
99
100How well a runner keeps work from its machine. A runner tells g1t its
101level when it registers and on every poll, and the **Runners** page shows
102it on each runner: *Sandboxed*, *VM per job*, *Shared Docker* or
103*Unsandboxed*.
104
105| Isolation | How you get it | What a job can reach |
106| --- | --- | --- |
107| `nested-container` | The runner's image under Sysbox, as above; on your own computer, `g1t-runner sandbox up`, which runs the same inside a Linux VM of the runner's own; or [the desktop app](/guides/desktop/#how-jobs-run)'s Sandboxed choice. | Its own container, and its own Docker Engine if it uses Docker. Not the machine, and not other jobs. |
108| `vm` | `g1t-runner sandbox up` on Windows 11 Pro, Enterprise or Education, or `register --native` there: jobs native to Windows, each in a [Windows Sandbox](#windows-jobs-in-windows-sandbox). | Its own virtual machine, made for the job and discarded after, with a read-only copy of the harness and no other folder of the machine's. |
109| `container` | `g1t-runner` installed on the machine with Docker, the desktop app's In Docker choice, or the image given the machine's Docker socket with the override. | Its own container. The runner itself drives the machine's Docker, which can do anything on the machine, so a job that escapes its container, or anything that takes over the runner, controls the machine. |
110| `host` | `--no-docker`, or the desktop app's Directly on this computer choice. Deprecated. | Everything the runner's user can: the machine, its files, and what earlier jobs left. |
111
112### What a job's container gets
113
114With `nested-container` and `container` isolation, each job runs in a
115container made for it and removed after it:
116
117| | |
118| --- | --- |
119| Image | The job's `container:`, else the runner's `--image`, else `node:24-bookworm`. |
120| Mounts | The harness (`g1t-runner`), read-only, and a volume of its own at `/home/runner`, removed after the job. Nothing else of the runner's or the machine's. |
121| Network | A Docker network of its own, removed after the job. Never the machine's network. |
122| Capabilities | Every one dropped but `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `FSETID`, `SETUID`, `SETGID`, `KILL` and `NET_BIND_SERVICE`, which installing packages and running test servers need. Raw sockets (`ping`) are among those dropped. |
123| Privileges | `no-new-privileges`: a setuid program such as `sudo` gains nothing. Run steps as the image's root user rather than with `sudo`. |
124| Limits | `--memory` (three quarters of the runner's memory unless you set it, with no swap beyond it), `--cpus` (every CPU unless you set it) and `--pids-limit` (4096 unless you set it). |
125| Credentials | Only the job's own short-lived token, passed by name. Never the runner's credential. |
126
127### Docker in jobs
128
129With `nested-container` isolation, a job that uses Docker (its
130`services:`, a Docker action, a `docker://` step, or `docker` in a step)
131gets a Docker Engine of its own. It starts the first time the job uses
132Docker, from `docker:28-dind`, beside the job's container and on the job's
133network, so a service's published ports are on the job's `localhost`. It
134is removed, with its images and containers, when the job ends. A job that
135does not use Docker never starts one.
136
137The job's engine is privileged inside the runner's container. Sysbox keeps
138it off the machine, but a job that uses Docker can reach the runner's own
139container, the runner's credential included. For code you do not trust,
140use [ephemeral runners](#autoscaling), so each job has a runner container
141of its own, or register with `--no-job-docker`, which gives jobs no Docker.
142
143With `container` isolation, jobs have no Docker of their own, since it
144would be the machine's: their `services:` are not started, and the log
145says so.
146
147### Host mode is deprecated
148
149`--no-docker` still works, and runs each job directly on the machine in a
150folder of its own. It will be removed. Until then:
151
152- The runner's log says so when it starts: *Deprecated: this runner runs
153 jobs directly on this machine (--no-docker), unsandboxed.*
154- Every job's log starts with a warning: *This job ran on the host,
155 unsandboxed …*
156- It is not given a public repository's work, which anyone can propose
157 code to. The runner says how it is isolated when it registers and on
158 every poll, and g1t hands a public repository's jobs only to
159 runners that are not in host mode. A job no other online runner
160 could take waits, and its page says *Waiting for a sandboxed runner:
161 this repository is public, and host-mode runners don't run public
162 repositories' jobs.* It starts once a sandboxed runner with its labels
163 comes online.
164- A runner from before this, which does not say how it is isolated, may
165 still be given such a job and refuses it itself: *acme/web is public,
166 and this self-hosted runner runs jobs directly on its machine
167 (--no-docker), unsandboxed, so it does not take a public repository's
168 work.* The job fails with that message. Update the runner.
169
170Move a Linux runner to [the container](#run-it-as-a-container), and a
171Windows or Mac computer to [the sandbox](#sandbox-on-your-computer). A
172runner registers as running jobs native to Windows or macOS (the
173`windows` or `macos` label) only when it sandboxes them (`--native`), or
174when you chose host mode with `--no-docker`.
175
176## Sandbox on your computer
177
178`g1t-runner sandbox` sets up self-hosted runners on your own desktop or
179laptop that nothing a job runs can get out of: not to your files, your
180other programs, your processes or your Docker. It gives the computer up
181to two capabilities, each a runner of its own on the **Runners** page:
182
183| Capability | Runner | Labels | Isolation |
184| --- | --- | --- | --- |
185| Linux jobs | `<name>-linux`, inside a Linux VM of the runner's own | `self-hosted`, `linux`, and the architecture | `nested-container` |
186| Jobs native to the computer's OS | `<name>-windows`, on the computer, starting a Windows Sandbox per job | `self-hosted`, `windows`, and the architecture | `vm` |
187
1881. Make a registration token: open **Runners** and click **New runner**.
1892. Download `g1t-runner` for your computer, as in [add a runner](#add-a-runner),
190 then run:
191
192 ```sh
193 g1t-runner sandbox up --url https://g1t.sh --token g1trt_…
194 ```
195
196The first `up` fetches the sandbox's image for this release, checks its
197signature and SHA-256, makes the VM, and registers both runners with the
198token; it takes a few minutes. After that, `up` starts what is stopped
199and needs no token.
200
201| Command | What it does |
202| --- | --- |
203| `g1t-runner sandbox up` | Makes the sandbox if there is none, and starts both runners. Waits until the Linux runner is asking for work. |
204| `g1t-runner sandbox down` | Stops both now. A running job is cut short, and its run says the runner was stopped. `--drain` waits for it to finish instead. `--delete` also removes the VM and everything in it; the runners stay listed on g1t until you remove them there. |
205| `g1t-runner sandbox down --only windows` | Stops Windows jobs alone and keeps them off: Linux jobs carry on, and a later `up` leaves Windows jobs stopped. `--only linux` does the same for Linux jobs, leaving Windows jobs running. Takes `--drain`, not `--delete`. |
206| `g1t-runner sandbox up --only windows` | Turns Windows jobs on again and starts them, leaving Linux jobs as they are. `--only linux,windows` turns on both. |
207| `g1t-runner sandbox status` | Says how each capability is, and what to do about one that is not available. |
208| `g1t-runner sandbox update` | Drains the Linux runner (the job it is running finishes), swaps in the newest release's image, and starts it again. The registration is kept, so it is the same runner on g1t. |
209
210| Option (`up`) | |
211| --- | --- |
212| `--url`, `--token` | Where to register and the registration token, for the first `up`. The token can come from `G1T_RUNNER_TOKEN` in the environment instead, which keeps it out of the process list. |
213| `--name` | The runners' name before `-linux` and `-windows`. The computer's name if left out. |
214| `--labels`, `--group` | As for [register](#register-options), for both runners. |
215| `--cpus` | The Linux VM's runner's CPUs, and so its jobs'. Half the computer's, at most 8, if left out. |
216| `--memory` | Its memory, such as `8g`. A quarter of the computer's, between 2 and 8 GB and never more than half, if left out. |
217| `--disk` | The VM's disk on macOS and Linux, such as `64g` (64 GB if left out). On Windows the disk is not limited: WSL can only resize it with all of WSL shut down. |
218| `--only` | The kinds of job to start, of `linux`, `windows` and `macos` (`native` is this computer's own): turned on, and kept on. The others are left as they are, running or not. Without it, `up` starts every kind that isn't turned off. `down` takes it too. |
219| `--no-native`, `--native` | Turn the native capability off, or on again, as `down --only` and `up --only` do. Kept until said otherwise. |
220| `--state-dir` | Where the sandbox keeps its settings and images. `~/.g1t-runner/sandbox` if left out. Every command takes it. |
221| `--json` | Print the status as JSON when done (see below). Every command takes it. |
222
223### How each computer runs it
224
225| Computer | Linux jobs | Native jobs |
226| --- | --- | --- |
227| Windows 11 Pro, Enterprise, Education | A WSL 2 distribution of the runner's own, `g1t-sandbox` | Windows Sandbox, one per job |
228| Windows 11 Home | A WSL 2 distribution of the runner's own, `g1t-sandbox` | Not available: Home has no Windows Sandbox |
229| macOS 13 or later | A VM under Apple's Virtualization framework, run by [vfkit](https://github.com/crc-org/vfkit) (`brew install vfkit`) | Not released: sandboxed macOS jobs are designed, not built |
230| Linux with Sysbox 0.7+ | No VM: the runner's image under Sysbox, as [above](#run-it-as-a-container) | None (Linux jobs are native) |
231| Linux without Sysbox | A VM under QEMU with KVM (`/dev/kvm`, and `qemu-system-x86_64` or `qemu-system-aarch64`) | None |
232
233Inside the VM (and on Linux with Sysbox) it is the runner's image under
234Sysbox, exactly as in [run it as a container](#run-it-as-a-container):
235each job is a fresh container on the runner's own Docker Engine, inside the
236runner's Sysbox container, inside the VM. Nothing listens on your
237computer; the VM only connects out.
238
239**What the VM gets of your computer:** no folders and no drives, no way
240to start its programs, no Docker socket, no clipboard, no GPU, display or
241sound, and no network port in. On Windows, the distribution's
242`/etc/wsl.conf` turns off WSL's drive mounts, its interop with Windows
243programs and Windows' `PATH`, and when it starts the sandbox also unmounts
244what WSL puts in every distribution anyway: `/mnt/wsl` (shared by all
245distributions, where Docker Desktop keeps sockets to its engine on
246Windows), WSLg's display and sound sockets, and Windows' GPU driver
247folders. It checks that nothing of the computer's is left, and refuses to
248run jobs if anything is. Containers in the VM, every job among them, may
249reach the internet but not your computer: a firewall rule in the VM drops
250their traffic to the computer's address, but for DNS.
251
252**Resources:** the runner's container has `--cpus` and `--memory`, and
253each job has its own limits inside that ([what a job's container gets](#what-a-jobs-container-gets)).
254On Windows these are the sandbox's share of WSL's own VM, which
255`%USERPROFILE%\.wslconfig` sizes for all of WSL.
256
257**Images and updates:** the sandbox's images are published with each
258runner release, at `g1t.sh/downloads/runner/<version>/`, and named with
259their SHA-256 in the release's manifest, which is signed with g1t's
260release key like the runner itself (see [updates](#updates)). The runner's
261image inside is named by digest in the same manifest. `update` checks the
262signature and every SHA-256 before using anything, and waits for the
263running job before it swaps the image. The runner's registration lives
264apart from the image (on its own disk on macOS and Linux; on Windows the
265launcher carries it across), so an update does not register again.
266
267### Windows jobs in Windows Sandbox
268
269With `vm` isolation, the runner on your computer takes jobs that ask for
270`runs-on: [self-hosted, windows]` and runs each in a Windows Sandbox of
271its own: a Hyper-V virtual machine with its own kernel, started fresh from
272your Windows and discarded, disk and all, when the job ends.
273
274- Its only folder from your computer is read-only and holds a copy of the
275 runner (the job's harness) and the job's script. Nothing it writes
276 outlives it.
277- No GPU, clipboard, printers, microphone or camera; networking on, so the
278 job can reach g1t and the internet.
279- A firewall rule in it blocks the address your computer is reached by.
280 The job runs as an administrator in its sandbox and could remove it, so
281 it slows a job down rather than stopping it; the wall is the VM.
282- One job at a time, as every runner.
283- It needs Windows 11 Pro, Enterprise or Education, version 24H2 or later
284 (for the `wsb` command), with **Windows Sandbox** turned on under
285 Settings, System, Optional features, More Windows features. `status`
286 says which of these is missing.
287
288On a runner that is not under `sandbox up`, `g1t-runner register --native`
289does the same.
290
291### What it cannot stop
292
293- **WSL 2 distributions share one VM.** On Windows, every WSL 2
294 distribution, your own among them, runs in one virtual machine with one
295 Linux kernel, and root in one can reach the others' disks. That is why
296 nothing a job runs is root in the sandbox's distribution: the runner is
297 a Sysbox container whose root is an unprivileged user there, and each job
298 a container inside it. A job would have to escape its container and the
299 runner's, and then reach root in the distribution, before it could touch
300 your other distributions, from which Windows is reachable.
301- **Mirrored networking.** With `networkingMode=mirrored` in
302 `.wslconfig`, Linux reaches Windows' own `localhost`, so the sandbox
303 will not start until it is removed.
304- **The computer's network.** Jobs reach the internet, and so the other
305 machines on your network that the internet could. Only your computer's
306 own address is blocked.
307- **The VM's own root** could reach your computer's services on its
308 address. Jobs are two containers away from it.
309- **A Windows job's sandbox** is as strong as Windows Sandbox: a Hyper-V
310 VM, with one read-only folder of your computer's.
311- **macOS and Linux VMs** (vfkit and QEMU) are built to the same design,
312 with no shared folders and nothing forwarded in, but have not yet been
313 run on a Mac or a Linux machine without Sysbox. Windows is where the
314 sandbox has been checked end to end.
315
316### What your computer can see
317
318The sandbox keeps jobs from reaching your computer. It does not keep your
319computer from looking at the sandbox, and so at what a job has while it
320runs:
321
322- **A job's log and secrets go from inside the sandbox straight to g1t.**
323 The runner inside fetches the job, its secrets and its `G1T_TOKEN`
324 itself, and sends its log back the same way, masked. None of it passes
325 through a log or file of your computer's.
326- **A [brokered secret](/guides/secrets-and-variables/#brokered-secrets)'s
327 value never reaches your computer at all.** The job has a stand-in, and
328 the runner passes the job's requests to the secret's hosts to g1t, which
329 fills the value in there and swaps it back out of the answer. This
330 works for jobs that run on Linux or macOS: in the sandbox's VM, in
331 Docker, or directly on the computer. A job on Windows keeps the
332 stand-in, and its log
333 warns that requests using it will not be signed in. See
334 [on a self-hosted runner](/guides/secrets-and-variables/#on-a-self-hosted-runner).
335- **Anything running as your account can look inside a running
336 sandbox**, as you can: open the WSL distribution with `wsl -u root`,
337 run a command in a Windows Sandbox, or read the VM's disk. So can an
338 administrator, root in another WSL distribution (all of WSL 2 shares one
339 VM), and on Linux with Sysbox anyone in the `docker` group. Don't run a
340 [host-mode](#host-mode-is-deprecated) runner, or software you don't
341 trust, under the account the sandbox runs as: what its jobs run could
342 read the sandbox's runner credentials and the secrets of the job
343 running in it. `g1t-runner sandbox status` warns when it finds a
344 host-mode runner registered for the same account (in `~/.g1t-runner`,
345 `G1T_RUNNER_DIR`, or the desktop app's folder for running jobs directly
346 on the computer). Remove it with `g1t-runner remove --dir <its folder>`,
347 or register it under another account.
348- **The job's token on your computer is spent after its first use.** A
349 workflow job reaches its runner with a token of its own, which sits
350 where your computer can see it: in a Windows job's script, in the job
351 folder Windows Sandbox reads, and in a container's environment. The
352 first thing the runner in the sandbox does is fetch the job with it,
353 and g1t answers with a new token that only the sandbox has; the first
354 stops working at once for everything that can be asked through g1t's
355 API (inside g1t, only the lookup of [brokered
356 secrets](/guides/secrets-and-variables/#brokered-secrets) still takes it, and that is not reachable
357 from your computer or a job). A copy left
358 behind can't fetch the job, its secrets or a `G1T_TOKEN` again. A job's
359 folder is deleted when the job ends, and a runner that was stopped
360 mid-job (a crash, or `taskkill`) deletes what it left when it next
361 starts.
362- **The registration token goes once the runner has registered.** It is
363 kept only until the runner inside has registered with it; then the
364 launcher takes it out of the VM's seed disk on macOS and Linux, and the
365 sandbox out of its own settings. On Windows, an `update` keeps the
366 runner's registration on your computer only while the VM is swapped.
367- **The state folders are yours alone.** The runner's folder
368 (`~/.g1t-runner`, `--dir` or `G1T_RUNNER_DIR`) and the sandbox's
369 (`--state-dir`) hold the runners' credentials, a Windows job's script
370 while it runs, and the VM's disk. On macOS and Linux they are made
371 `0700` and their files `0600`. On Windows a folder in your profile is
372 already yours alone; one elsewhere, such as `C:\runner`, would let every
373 user of the computer read it, so the runner makes it private (you,
374 SYSTEM and Administrators, and nothing inherited from the folder
375 above). If it can't, it refuses to start and says how to fix it.
376- **On Windows, opening the sandbox's distribution without naming a user**
377 (Explorer at `\\wsl.localhost\g1t-sandbox`, or `wsl -d g1t-sandbox`)
378 gets a locked account with no shell, which can't read the runner's
379 registration. The launcher names root itself for what it does there.
380
381### The status as JSON
382
383`g1t-runner sandbox status --json` prints one JSON object, which [the
384desktop app](/guides/desktop/) shows. It exits 0 whenever it could read
385the status, a sandbox in error included; `up`, `down` and `update` print
386the same object with `--json` when they are done, and exit 1 with
387`error` set when they fail.
388
389```json
390{
391 "schema": 1,
392 "state": "running",
393 "backend": "wsl2",
394 "runner_version": "0.3.0",
395 "launcher_version": "0.3.0",
396 "image_version": "0.3.0",
397 "isolation": "nested-container",
398 "job": { "kind": "workflow", "id": "job_…", "name": "build", "repo": "acme/web" },
399 "error": null,
400 "resources": { "cpus": 4, "memory_mib": 8192, "disk_gib": 64 },
401 "capabilities": [
402 { "id": "linux", "available": true, "state": "running", "isolation": "nested-container",
403 "runner_version": "0.3.0", "job": null, "cause": null, "fix": null, "turned_off": false },
404 { "id": "windows", "available": false, "state": "absent", "isolation": "vm",
405 "runner_version": null, "job": null, "turned_off": false,
406 "cause": "Windows Sandbox is not turned on, or this edition of Windows has none (Home does not).",
407 "fix": "On Windows 11 Pro, Enterprise or Education, turn on Windows Sandbox under Settings, System, Optional features, More Windows features, then restart." }
408 ],
409 "state_dir": "C:\\Users\\you\\.g1t-runner\\sandbox",
410 "warnings": []
411}
412```
413
414| Field | |
415| --- | --- |
416| `state` | The Linux VM's: `absent` (never made, or deleted), `stopped`, `starting`, `running`, `updating` or `error`. |
417| `backend` | What holds it: `wsl2`, `vfkit`, `qemu` or `sysbox`. |
418| `runner_version`, `image_version`, `launcher_version` | The runner inside the VM, the VM's image, and this `g1t-runner`. |
419| `job` | What the Linux runner is running now, or null. |
420| `error` | A sentence for a person when something is wrong, or null. |
421| `capabilities` | Each kind of job the computer can run sandboxed: `linux`, and `windows` or `macos`. `available` says whether it can be had here; when not, `cause` says why and `fix` what to do. Each has its own `state`, `job` and `runner_version`. A native one whose runner didn't start is in `error`, with why in `cause`. |
422| `turned_off` | On each capability: someone stopped it (`down --only`, `--no-native`). Its `state` is then `stopped`: stopped by a person, not failing, and nothing in `error`. |
423| `warnings` | Sentences about what on the computer weakens the sandbox ([what your computer can see](#what-your-computer-can-see)): a host-mode runner registered for the same account, or a state folder other users can read. Empty when nothing does; left out by earlier versions of `g1t-runner`. `status` without `--json` prints each as a line starting `Warning:`. |
424
425Fields are only added; `schema` changes if one changes meaning.
426
427## The Runners page
428
429A workspace's **Runners** page shows g1t cloud and your own runners side
430by side, with what each is running now and this month's time on each.
431
432| Part | What it shows |
433| --- | --- |
434| **g1t cloud** | How many workflow jobs are running in g1t's sandboxes now, this month's runner minutes and what they came to at price, and each machine's price a minute ([runner minutes](/guides/usage-and-billing/#runner-minutes)). |
435| **Your runners** | How many are online, busy, idle and offline, this month's self-hosted time (always $0), how many jobs are waiting for one of them, and whether they take [g1t's work](#g1ts-work-on-your-runners). |
436| **On g1t cloud now** | Each running workflow job, linked to its run, with its kind, project and when it started. Work handed to one of your runners is listed under that runner instead. |
437| **Machine time** | This month's time on g1t cloud against your runners', from your [usage](/guides/usage-and-billing/). |
438| **Your runners** list | Each runner's status, labels, OS, architecture, `g1t-runner` version and when it was last heard from, and what it is running, linked to its run. |
439| **Where work runs** | What decides where each kind of work runs, and whether [g1t's work](#g1ts-work-on-your-runners) runs on your runners. |
440
441A runner is **Idle** when it is online with nothing to do, **Busy** while it
442runs a job, and **Offline** when it has not polled for 90
443seconds. When a service does not answer, only its part of the page says so.
444
445**Desktops** lists every computer in the workspace with [the desktop
446app](/guides/desktop/#the-runner), whose runner runs while g1t is open
447there: what each is doing, or why it isn't running and how to fix it, in
448the app's own words, and how it runs jobs. A computer whose g1t has been
449closed for ten minutes says *g1t is closed on* its name, and so does its
450runner in the list, rather than *Offline*.
451
452## A runner in the desktop app
453
454[The desktop app](/guides/desktop/#the-runner) runs a runner for you
455while it is open, with nothing to install or register: it registers your
456computer with the workspace you choose, as you, through the site, and
457keeps the runner up to date. It runs jobs sandboxed by default
458(`nested-container`); running them in Docker (`container`) or directly
459on the computer (`host`) are settings you choose, and the app never falls
460back to them. Sandboxed, it is [the sandbox](#sandbox-on-your-computer),
461which the app sets up with `g1t-runner sandbox up` and shows with
462`sandbox status --json`: Linux jobs and, on Windows 11 Pro and up,
463Windows jobs, each a runner of its own. Its runners have the label
464`desktop` besides the usual ones.
465
466## Add a runner
467
468On Linux, [run it as a container](#run-it-as-a-container). To install
469`g1t-runner` on a machine instead, as on macOS and Windows:
470
4711. Open **Runners** (a project's: **Settings → Runners**) and click **New runner**. g1t makes a
472 registration token and shows the commands for Linux, macOS, Windows and
473 Docker with it filled in. The token lasts an hour, can register any
474 number of runners until then, and is shown once.
4752. On the machine, download `g1t-runner` and register it:
476
477 <Tabs syncKey="os">
478 <TabItem label="macOS">
479 ```sh
480 # Apple silicon; use g1t-runner-macos-x64 on Intel
481 curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-macos-arm64
482 chmod +x g1t-runner
483 ./g1t-runner register --url https://g1t.sh --token g1trt_…
484 ```
485 </TabItem>
486 <TabItem label="Linux">
487 ```sh
488 # x64; use g1t-runner-linux-arm64 on arm64
489 curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-linux-x64
490 chmod +x g1t-runner
491 ./g1t-runner register --url https://g1t.sh --token g1trt_…
492 ```
493 </TabItem>
494 <TabItem label="Windows">
495 ```powershell
496 Invoke-WebRequest https://g1t.sh/downloads/runner/latest/g1t-runner-windows-x64.exe -OutFile g1t-runner.exe
497 .\g1t-runner.exe register --url https://g1t.sh --token g1trt_…
498 ```
499 </TabItem>
500 </Tabs>
501
5023. Start it: `./g1t-runner run` keeps it running in the terminal, or
503 install it as a service that starts with the machine:
504
505 <Tabs syncKey="os">
506 <TabItem label="macOS">
507 ```sh
508 ./g1t-runner service install
509 ```
510 A launch agent, `sh.g1t.g1t-runner-<name>`; its log is `~/.g1t-runner/runner.log`.
511 </TabItem>
512 <TabItem label="Linux">
513 ```sh
514 sudo ./g1t-runner service install
515 ```
516 A systemd unit, `g1t-runner-<name>`; its log is `journalctl -u g1t-runner-<name>`.
517 </TabItem>
518 <TabItem label="Windows">
519 From an Administrator prompt:
520 ```powershell
521 .\g1t-runner.exe service install
522 ```
523 A Windows service, `g1t-runner-<name>`, that restarts if it stops.
524 </TabItem>
525 </Tabs>
526
527 `service start`, `stop`, `status` and `uninstall` do what they say.
528
529The runner shows up in the list within a few seconds, with a green dot.
530
531Installed this way, it has `container` isolation with Docker, or `host`
532isolation with `--no-docker`. See [isolation](#isolation).
533
534### Register options
535
536| Option | |
537| --- | --- |
538| `--url` | The g1t you register with: `https://g1t.sh`. |
539| `--token` | The registration token. |
540| `--name` | What to call it. The machine's name if left out. Names are unique within a workspace (or project). |
541| `--labels` | Extra labels, comma-separated: `gpu,cuda-12`. See [labels](#labels). |
542| `--group` | A workspace runner's [group](#groups). The default group, or the one the token was made for, if left out. |
543| `--ephemeral` | Take one job, then remove itself. For [autoscaling](#autoscaling). |
544| `--no-docker` | Deprecated. Run jobs directly on the machine instead of in containers ([host mode](#host-mode-is-deprecated)). |
545| `--native` | Run jobs native to this OS, each in a throwaway VM of its own: [Windows Sandbox](#windows-jobs-in-windows-sandbox) on Windows. Registers with the OS's label and `vm` isolation, and refuses if the sandbox is not there. |
546| `--image` | The image workflow jobs run in when they name no `container:`. `node:24-bookworm` if left out. |
547| `--memory` | Each job container's memory, such as `8g`. Three quarters of the runner's memory if left out. |
548| `--cpus` | Each job container's CPUs, such as `4` or `1.5`. Every CPU if left out. |
549| `--pids-limit` | The most processes in a job container. 4096 if left out. |
550| `--no-job-docker` | Give jobs no [Docker of their own](#docker-in-jobs). |
551| `--job-docker-image` | The image a job's own Docker Engine runs. `docker:28-dind` if left out. |
552| `--work-dir` | Where jobs run with `--no-docker` keep their files. `~/.g1t-runner/work` if left out. |
553| `--dir` | Where the runner keeps its configuration and credential. `~/.g1t-runner`, or `G1T_RUNNER_DIR`, if left out. Every command takes it. |
554| `--replace` | Take the place of a runner with the same name, registered from any computer. A runner registering again from the computer it registered from takes its name back without it ([registering again](#registering-again)). |
555| `--machine` | Which computer this is, as 32 to 128 hex digits, for a runner that can't tell ([registering again](#registering-again)). Worked out from the operating system if left out. |
556| `--no-auto-update` | Never update itself. |
557
558### Registering again
559
560A runner says which computer it is when it registers: a fingerprint made
561from the operating system's machine id (Windows' `MachineGuid`, macOS's
562platform UUID, Linux's `/etc/machine-id`), hashed so the id itself never
563leaves the computer, and kept by g1t hashed again with your workspace. When
564a computer registers a runner under a name one of its own runners already
565has, because its sandbox was made again or its `--dir` was lost, the new
566registration takes the old one's place, with no `--replace`. A runner of
567the same name from another computer is left alone.
568
569A runner in a container can't tell which computer it is on, since its
570machine id is its image's, so it sends no fingerprint unless given one
571with `--machine` or `G1T_RUNNER_MACHINE`. `g1t-runner sandbox` and the
572desktop app pass the computer's to the runner in the sandbox.
573
574A runner registered before runners sent a fingerprint is taken over when
575the person who made the new registration token made its token too, and it
576has been offline for more than 90 seconds.
577
578To run several runners on one machine, give each its own `--dir` and
579`--name`.
580
581## Use it in a workflow
582
583A job runs on a self-hosted runner when its `runs-on` names `self-hosted`:
584
585```yaml
586jobs:
587 test:
588 runs-on: self-hosted
589 steps:
590 - uses: actions/checkout@v5
591 - run: make test
592
593 gpu:
594 runs-on: [self-hosted, linux, gpu]
595 steps:
596 - run: nvidia-smi
597
598 windows:
599 runs-on: [self-hosted, windows]
600 steps:
601 - run: Get-ComputerInfo | Select-Object OsName
602```
603
604Until a runner with every label the job names takes it, the job waits, and
605its page says what for: *Waiting for a self-hosted runner with labels
606self-hosted, linux, gpu.* A job that waits a day fails.
607
608`runs-on: { group: GPU, labels: [linux] }` asks for a runner in the group
609called GPU as well.
610
611### Labels
612
613Every runner has `self-hosted`, its OS (`linux`, `macos` or `windows`) and
614its architecture (`x64` or `arm64`), then the labels you gave it. A runner
615that runs jobs in Docker is `linux`, whatever the machine is, since that is
616what its jobs run on. Labels are matched without regard to case.
617
618A job takes a runner only when every label in its `runs-on` is one of the
619runner's. Labels that name g1t's own machines, such as `ubuntu-latest` or
620`g1t-4core`, never go to a self-hosted runner.
621
622### What a job gets
623
624A job on your runner runs exactly as it would in g1t's sandbox: the same
625harness, the same `${{ }}` contexts, `GITHUB_*` variables, secrets and
626workflow commands, the same log and artifacts. `runner.name`, `runner.os`
627and `runner.arch` are the machine's, and `RUNNER_ENVIRONMENT` is
628`self-hosted`.
629
630- **In a container** (the default), each job gets a fresh container from
631 its `container:` image or the runner's `--image`, removed when it ends,
632 with the walls in [what a job's container gets](#what-a-jobs-container-gets).
633 In the runner's image under Sysbox, its `services:` and Docker actions
634 get [a Docker Engine of its own](#docker-in-jobs).
635- **With `--no-docker`** (deprecated), each job gets a fresh folder under
636 the work folder, removed when it ends, and runs with whatever the
637 machine has installed. A step's default shell is `bash` on Linux and
638 macOS and PowerShell on Windows; `shell: pwsh`, `powershell`, `cmd`,
639 `bash` and `python` work where installed. On a machine with Docker, the
640 job's `services:`, `container:`, `docker://` steps and Docker actions
641 use the machine's Docker, as on GitHub's runners.
642
643A self-hosted job stops at 360 minutes unless its `timeout-minutes` says
644otherwise, as on GitHub, and can ask for up to 24 hours (1440). Jobs on
645g1t's own machines stop at 60 minutes.
646
647## Groups
648
649A workspace's runners are in groups, which say which repositories may use
650them. Every workspace has a **Default** group, for every repository, which
651runners join unless their token or `--group` names another.
652
653To keep a set of machines for some repositories, open the workspace's
654**Runners** page, click **New group**, name it, and choose **Only these**
655repositories. Then click **New runner** with that group chosen, or register
656with `--group`. Deleting a group moves its runners to the default group.
657
658## g1t's work on your runners
659
660**Runners → Where work runs** can send g1t's own work to your runners
661too: the checks g1t runs on pull requests, merge checks and the merge
662queue's test runs. Switch on **Run g1t's work on self-hosted runners** and
663give the labels a runner needs to take it (`self-hosted` is always one).
664
665- The work runs exactly as in g1t's sandbox: the same harness, with a
666 short-lived credential that can do only what that kind of run may, in
667 that repository, revoked when it ends.
668- Its machine time is free, as for every job on your runners.
669
670A project can have its own setting, or follow the workspace's.
671
672## Billing
673
674Time on your runners is free. Each job's minutes go on
675[usage](/guides/usage-and-billing/) as **Self-hosted runner time** at $0,
676so you can see how much ran there.
677
678- Workflow jobs on your runners need no plan and no card: they work on the
679 free tier. A workspace on the free plan runs **4** of its workflow jobs
680 on its runners at once, however many runners it has; the rest stay
681 queued until one finishes. On the g1t plan there is no such cap.
682- A workspace g1t has put a hold on cannot register new runners: making a
683 registration token answers that it is held.
684
685## Security
686
687- **Pull requests from forks never run on your runners** unless you allow
688 it under **Runners → Where work runs**. Leave it off on a
689 public repository: anyone who can open a pull request could run any code
690 on the machine, read what it can reach, and leave something behind for
691 the next job. Such jobs get no secrets either way.
692- **Secrets** reach a job on your runner under the same rules as on g1t's:
693 only trusted runs get them, and only the ones for the job's environment.
694- **The runner's credential** can poll for work, report on what it was
695 given and remove itself, and nothing else; every other endpoint refuses
696 it. It is kept only as a hash on g1t's side, rotates every day, and stops
697 working the moment the runner is removed. It is never given to a job:
698 each job gets only its own short-lived token, which the job spends on
699 fetching itself and replaces with one only the job has. The runner's
700 folder is its owner's alone, and the runner refuses to start in one
701 other users can read ([what your computer can see](#what-your-computer-can-see)).
702- **Isolation**: run the runner's image under Sysbox. Each job then gets
703 a container of its own inside the runner's container, removed after,
704 and neither reaches the machine. A job with `--no-docker` runs as the
705 runner's user and can reach whatever that user can; host mode is
706 deprecated and refuses public repositories' work. Use ephemeral runners
707 for code you do not trust. See [isolation](#isolation).
708- **Network**: a job reaches whatever the machine can.
709- **Audit**: making a registration token, registering, removing, and every
710 job a runner takes are in the [audit log](/guides/audit-log/), with the
711 runner as the actor.
712- Only owners (or a repository's admins) register and remove runners,
713 signed in or with a person's token with `runners:admin`. Workspace
714 tokens, `G1T_TOKEN` included, cannot, so a workflow cannot add a machine
715 to run its own jobs.
716
717## Docker and Kubernetes
718
719[Run it as a container](#run-it-as-a-container) covers Docker. The image's
720settings, besides the register options:
721
722| Variable | |
723| --- | --- |
724| `G1T_RUNNER_SANDBOX` | `nested` in the image: start a Docker Engine inside the container and run jobs on it. |
725| `G1T_RUNNER_DIR` | `/data` in the image: where the configuration and credential are kept. |
726| `G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS` | `1` starts the runner even when it can reach the machine. See [above](#run-it-as-a-container). |
727
728A runner that ran the image with the machine's Docker socket, as these
729docs said before, refuses to start with the new image. Remove the
730`-v /var/run/docker.sock:/var/run/docker.sock` and add
731`--runtime=sysbox-runc`; the `/data` volume keeps its registration.
732
733### Autoscaling
734
735Ephemeral runners take one job and remove themselves, so each job starts in
736a fresh runner container. In Kubernetes with Sysbox installed on the
737nodes (its Kubernetes install adds the `sysbox-runc` runtime class), run
738them in pods that make their own registration token from an owner's
739personal [access token](/guides/authentication/#access-tokens) with only
740`runners:admin`, and let the Deployment replace each pod that finishes:
741
742```yaml
743apiVersion: apps/v1
744kind: Deployment
745metadata:
746 name: g1t-runner
747spec:
748 replicas: 3
749 selector: { matchLabels: { app: g1t-runner } }
750 template:
751 metadata:
752 labels: { app: g1t-runner }
753 annotations: { io.kubernetes.cri-o.userns-mode: "auto:size=65536" }
754 spec:
755 runtimeClassName: sysbox-runc
756 containers:
757 - name: runner
758 image: g1t.sh/flagon-io/g1t-runner
759 command: ["sh", "-c"]
760 args:
761 - |
762 export G1T_RUNNER_TOKEN="$(curl -fsS -X POST \
763 -H "Authorization: Bearer $G1T_ADMIN_TOKEN" \
764 https://api.g1t.sh/workspaces/acme/actions/runners/registration-token \
765 | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
766 exec g1t-runner register-and-run
767 env:
768 - { name: G1T_RUNNER_URL, value: https://g1t.sh }
769 - { name: G1T_RUNNER_LABELS, value: k8s }
770 - { name: G1T_RUNNER_EPHEMERAL, value: "1" }
771 - name: G1T_ADMIN_TOKEN
772 valueFrom: { secretKeyRef: { name: g1t, key: runners-admin-token } }
773```
774
775To scale with demand rather than keep a fixed number, change `replicas`
776with your autoscaler of choice: [`list_runners`](/reference/api/runners/list-runners-for-workspace/)
777says which are busy.
778
779## Updates
780
781The runner checks for a new release every six hours while it is idle, and
782updates itself when the release is signed by g1t's release key and its
783download matches the release's SHA-256. `g1t-runner update` does it now;
784`--no-auto-update` turns it off. Releases are at
785`https://g1t.sh/downloads/runner/<version>/`, with a `SHA256SUMS` file.
786The runner inside [the sandbox](#sandbox-on-your-computer) does not update
787itself: `g1t-runner sandbox update` swaps the whole sandbox's image, which
788carries it, once the running job is done.
789
790## Remove a runner
791
792On the machine, `g1t-runner remove` unregisters it and forgets its
793credential (run `service uninstall` first if it is a service). Or remove it
794from the **Runners** page; the runner stops on its next poll. A job it was
795running fails. Runners offline for 14 days are removed by themselves.
796
797## API and MCP
798
799| What | REST | MCP (`workflow` tool) | Scope |
800| --- | --- | --- | --- |
801| 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` |
802| Make a registration token | [`POST …/actions/runners/registration-token`](/reference/api/runners/create-runner-registration-token-for-workspace/) | `create_runner_token` | `runners:admin` |
803| Remove a runner | [`DELETE …/actions/runners/{id}`](/reference/api/runners/remove-runner-for-workspace/) | `remove_runner` | `runners:admin` |
804| 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` |
805| 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` |
806
807## Troubleshooting
808
809| What you see | What to do |
810| --- | --- |
811| 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 the **Runners** page, check the runner's group, or start the runner. |
812| *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. |
813| `register` says the token is not valid | It expired after an hour, or was mistyped. Make a new one. |
814| `register` says a runner with that name is registered | Another computer's runner has that name, or one registered before the computer was known. Choose another `--name`, or add `--replace`. |
815| `run` says *g1t no longer knows this runner* | It was removed, or its credential was. Register it again. |
816| *this runner is not sandboxed from its machine, so it will not take work* | The runner's container can reach the machine: it lists why. Remove the machine's Docker socket and `--privileged`, and run it with `--runtime=sysbox-runc`. See [run it as a container](#run-it-as-a-container). |
817| *the runner's own Docker Engine could not start* | The container is not running under Sysbox. Add `--runtime=sysbox-runc`, and check that Sysbox is installed (`docker info` lists `sysbox-runc` under runtimes). |
818| The job says *Waiting for a sandboxed runner: this repository is public …* | Every online runner with the job's labels is in host mode, which never runs a public repository's jobs. Run a runner's image under Sysbox, or register one without `--no-docker`. |
819| *… is public, and this self-hosted runner runs jobs directly on its machine* | An older runner in host mode refused a public repository's job. Update it, so g1t gives the job to a sandboxed runner instead, and run the runner's image under Sysbox. |
820| `sandbox up` says *WSL is not installed* | Open a terminal as administrator, run `wsl --install --no-distribution`, restart, and run `sandbox up` again. That is the one step that needs an administrator. |
821| `sandbox up` says *WSL is set to mirrored networking* | Remove `networkingMode=mirrored` from `%USERPROFILE%\.wslconfig`, run `wsl --shutdown`, and try again. |
822| `sandbox status` says *this sandbox can reach the computer* | Something of your computer's is mounted in the sandbox's VM, or an engine's socket is there; it lists what. Run `g1t-runner sandbox down --delete` and `up` again, which makes it from the image afresh. |
823| `sandbox status` shows the Linux runner in `error` with *The runner stopped …* | The runner inside could not register or start; the message is its last line. Check the token was still valid (an hour), and that the computer reaches `https://api.g1t.sh`. |
824| The `windows` capability is unavailable | `status` says why: Windows Sandbox is off, this is Windows 11 Home, or Windows is older than 24H2. |
825| A step's `sudo` fails | Job containers have `no-new-privileges`. Steps run as the image's user, root in most CI images, so drop the `sudo`. |
826| *could not run docker* | Install Docker and start it, and give the runner's user access to it. |
827| 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`. |