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