Skip to content
584 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 macOS and
21 Windows, sandboxed jobs are [not built yet](#windows-and-macos).
22
23| Where | Page | Who manages it |
24| --- | --- | --- |
25| A workspace | **Workspace → Runners** in the rail, `g1t.sh/<workspace>/-/runners` | Owners only. |
26| A project | **Settings → Runners**, `g1t.sh/<workspace>/<project>/settings/runners` | People with the Admin [role](/guides/access-and-roles/) on its repository |
27
28A workspace's runners serve the repositories their [group](#groups) allows.
29A project's own runners serve only that project.
30
31## Run it as a container
32
33On a Linux machine, run the runner's image with the Sysbox runtime. The
34runner starts a Docker Engine of its own inside its container and runs
35each job in a container on that engine, so jobs are containers inside the
36runner's container. Sysbox gives the runner's container a user namespace
37of its own, so its root is an unprivileged user on the machine, and lets
38it run Docker without `--privileged` and without the machine's Docker
39socket.
40
411. Install [Sysbox](https://github.com/nestybox/sysbox) on the machine,
42 following its install guide. It needs Linux; Docker Desktop on macOS
43 and Windows cannot use it.
442. Make a registration token: open **Runners** and click **New runner**.
453. Run the runner:
46
47 ```sh
48 docker run -d --name g1t-runner --restart unless-stopped \
49 --runtime=sysbox-runc \
50 -v g1t-runner:/data \
51 -e G1T_RUNNER_URL=https://g1t.sh \
52 -e G1T_RUNNER_TOKEN=g1trt_… \
53 g1t.sh/flagon-io/g1t-runner
54 ```
55
56The image runs `register-and-run`, which registers once and then runs.
57Its configuration and credential are kept on the `g1t-runner` volume, so
58the container can be restarted and replaced without registering again.
59Every [register option](#register-options) can come from
60`G1T_RUNNER_<OPTION>` in the environment, such as `G1T_RUNNER_LABELS=gpu`
61or `G1T_RUNNER_MEMORY=8g`, so a secret can hold the token.
62
63Its log says which isolation it has:
64
65```text
66… 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.
67```
68
69Do not give the container the machine's Docker socket
70(`-v /var/run/docker.sock:/var/run/docker.sock`) or `--privileged`. The
71runner looks for both when it starts, and refuses to start if it finds
72them:
73
74| It finds | What it says |
75| --- | --- |
76| 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* |
77| 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* |
78| `DOCKER_HOST` set | *DOCKER_HOST is set …* |
79| Root in the container is root on the machine (no Sysbox, or `--privileged`) | *root in this container is root on the machine* |
80
81To start anyway, with jobs able to reach the machine, set
82`G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS=1`. The runner then logs each
83problem with `UNSAFE:` in front, and registers as `container` rather than
84`nested-container`.
85
86The engine's images and build cache are kept in the container, so they
87last until the container is removed. Sysbox keeps them on the machine's
88disk for that container alone; to keep them when you replace the
89container, add `-v g1t-runner-docker:/var/lib/docker`, a volume for one
90runner container at a time.
91
92## Isolation
93
94How well a runner keeps work from its machine. A runner tells g1t its
95level when it registers.
96
97| Isolation | How you get it | What a job can reach |
98| --- | --- | --- |
99| `nested-container` | The runner's image under Sysbox, as above, 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. |
100| `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. |
101| `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. |
102
103### What a job's container gets
104
105With `nested-container` and `container` isolation, each job runs in a
106container made for it and removed after it:
107
108| | |
109| --- | --- |
110| Image | The job's `container:`, else the runner's `--image`, else `node:24-bookworm`. |
111| 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. |
112| Network | A Docker network of its own, removed after the job. Never the machine's network. |
113| 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. |
114| 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. |
115| 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). |
116| Credentials | Only the job's own short-lived token, passed by name. Never the runner's credential. |
117
118### Docker in jobs
119
120With `nested-container` isolation, a job that uses Docker (its
121`services:`, a Docker action, a `docker://` step, or `docker` in a step)
122gets a Docker Engine of its own. It starts the first time the job uses
123Docker, from `docker:28-dind`, beside the job's container and on the job's
124network, so a service's published ports are on the job's `localhost`. It
125is removed, with its images and containers, when the job ends. A job that
126does not use Docker never starts one.
127
128The job's engine is privileged inside the runner's container. Sysbox keeps
129it off the machine, but a job that uses Docker can reach the runner's own
130container, the runner's credential included. For code you do not trust,
131use [ephemeral runners](#autoscaling), so each job has a runner container
132of its own, or register with `--no-job-docker`, which gives jobs no Docker.
133
134With `container` isolation, jobs have no Docker of their own, since it
135would be the machine's: their `services:` are not started, and the log
136says so.
137
138### Host mode is deprecated
139
140`--no-docker` still works, and runs each job directly on the machine in a
141folder of its own. It will be removed. Until then:
142
143- The runner's log says so when it starts: *Deprecated: this runner runs
144 jobs directly on this machine (--no-docker), unsandboxed.*
145- Every job's log starts with a warning: *This job ran on the host,
146 unsandboxed …*
147- It is not given a public repository's work, which anyone can propose
148 code to. The runner says how it is isolated when it registers and on
149 every poll, and g1t hands a public repository's jobs and agent work
150 only to runners that are not in host mode. A job no other online runner
151 could take waits, and its page says *Waiting for a sandboxed runner:
152 this repository is public, and host-mode runners don't run public
153 repositories' jobs.* It starts once a sandboxed runner with its labels
154 comes online.
155- A runner from before this, which does not say how it is isolated, may
156 still be given such a job and refuses it itself: *acme/web is public,
157 and this self-hosted runner runs jobs directly on its machine
158 (--no-docker), unsandboxed, so it does not take a public repository's
159 work.* The job fails with that message. Update the runner.
160
161Move a Linux runner to [the container](#run-it-as-a-container). A macOS
162or Windows runner stays in host mode until sandboxes for them are built.
163
164### Windows and macOS
165
166Sandboxed jobs on Windows and macOS are not built yet; jobs native to
167those systems run in host mode. What is planned:
168
169| | How each job will run |
170| --- | --- |
171| Windows | A Windows container with Hyper-V isolation: a small virtual machine with its own kernel, made from a signed image for the job and destroyed after it. |
172| macOS | An ephemeral macOS virtual machine, using Apple's Virtualization framework, cloned from a signed image for the job and deleted after it. Apple's licence allows two macOS virtual machines per Mac at once, so a Mac will run at most two jobs at a time. |
173
174On both, the only part on the machine itself will be a small launcher that
175makes and destroys those sandboxes from signed images; the runner and the
176job run inside them.
177
178On a Mac or Windows machine with Docker Desktop, a runner installed there
179without `--no-docker` already runs jobs in Linux containers, with
180`container` isolation.
181
182## The Runners page
183
184A workspace's **Runners** page shows g1t cloud and your own runners side
185by side, with what each is running now and this month's time on each.
186
187| Part | What it shows |
188| --- | --- |
189| **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. |
190| **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. |
191| **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. |
192| **Machine time** | This month's time on g1t cloud against your runners', from your [usage](/guides/usage-and-billing/). |
193| **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. |
194| **Where work runs** | What decides where each kind of work runs, and the [agent setting](#agents-on-your-runners). |
195
196A runner is **Idle** when it is online with nothing to do, **Busy** while it
197runs a job or an agent run, and **Offline** when it has not polled for 90
198seconds. When a service does not answer, only its part of the page says so.
199
200Sessions that persist are here for workspace agents: each has
201[a computer of its own](/guides/agents/#its-computer) on g1t cloud, whose
202home stays between sessions and which sleeps when idle; the page says so
203under **Agents' computers and more**. Pinning an agent's computer to one of
204your own runners and an official agent image are coming, and the page
205labels them so.
206
207**Desktops** lists every computer in the workspace with [the desktop
208app](/guides/desktop/#the-runner), whose runner runs while g1t is open
209there: what each is doing, or why it isn't running and how to fix it, in
210the app's own words, and how it runs jobs. A computer whose g1t has been
211closed for ten minutes says *g1t is closed on* its name, and so does its
212runner in the list, rather than *Offline*.
213
214## A runner in the desktop app
215
216[The desktop app](/guides/desktop/#the-runner) runs a runner for you
217while it is open, with nothing to install or register: it registers your
218computer with the workspace you choose, as you, through the site, and
219keeps the runner up to date. It runs jobs sandboxed by default
220(`nested-container`); running them in Docker (`container`) or directly
221on the computer (`host`) are settings you choose, and the app never falls
222back to them. Its runner has the label `desktop` besides the usual ones.
223
224## Add a runner
225
226On Linux, [run it as a container](#run-it-as-a-container). To install
227`g1t-runner` on a machine instead, as on macOS and Windows:
228
2291. Open **Runners** (a project's: **Settings → Runners**) and click **New runner**. g1t makes a
230 registration token and shows the commands for Linux, macOS, Windows and
231 Docker with it filled in. The token lasts an hour, can register any
232 number of runners until then, and is shown once.
2332. On the machine, download `g1t-runner` and register it:
234
235 ```sh
236 # Linux (x64; use g1t-runner-linux-arm64 on arm64)
237 curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-linux-x64
238 chmod +x g1t-runner
239 ./g1t-runner register --url https://g1t.sh --token g1trt_…
240 ```
241
242 ```sh
243 # macOS (Apple silicon; use g1t-runner-macos-x64 on Intel)
244 curl -fsSLo g1t-runner https://g1t.sh/downloads/runner/latest/g1t-runner-macos-arm64
245 chmod +x g1t-runner
246 ./g1t-runner register --url https://g1t.sh --token g1trt_…
247 ```
248
249 ```powershell
250 # Windows (PowerShell)
251 Invoke-WebRequest https://g1t.sh/downloads/runner/latest/g1t-runner-windows-x64.exe -OutFile g1t-runner.exe
252 .\g1t-runner.exe register --url https://g1t.sh --token g1trt_…
253 ```
254
2553. Start it: `./g1t-runner run` keeps it running in the terminal, or
256 install it as a service that starts with the machine:
257
258 | OS | Command | What it makes |
259 | --- | --- | --- |
260 | Linux | `sudo ./g1t-runner service install` | A systemd unit, `g1t-runner-<name>`; its log is `journalctl -u g1t-runner-<name>` |
261 | macOS | `./g1t-runner service install` | A launch agent, `sh.g1t.g1t-runner-<name>`; its log is `~/.g1t-runner/runner.log` |
262 | Windows | `.\g1t-runner.exe service install`, from an Administrator prompt | A Windows service, `g1t-runner-<name>`, that restarts if it stops |
263
264 `service start`, `stop`, `status` and `uninstall` do what they say.
265
266The runner shows up in the list within a few seconds, with a green dot.
267
268Installed this way, it has `container` isolation with Docker, or `host`
269isolation with `--no-docker`. See [isolation](#isolation).
270
271### Register options
272
273| Option | |
274| --- | --- |
275| `--url` | The g1t you register with: `https://g1t.sh`. |
276| `--token` | The registration token. |
277| `--name` | What to call it. The machine's name if left out. Names are unique within a workspace (or project). |
278| `--labels` | Extra labels, comma-separated: `gpu,cuda-12`. See [labels](#labels). |
279| `--group` | A workspace runner's [group](#groups). The default group, or the one the token was made for, if left out. |
280| `--ephemeral` | Take one job, then remove itself. For [autoscaling](#autoscaling). |
281| `--no-docker` | Deprecated. Run jobs directly on the machine instead of in containers ([host mode](#host-mode-is-deprecated)). |
282| `--image` | The image workflow jobs run in when they name no `container:`. `node:24-bookworm` if left out. |
283| `--agent-image` | The image [agent work](#agents-on-your-runners) runs in. |
284| `--memory` | Each job container's memory, such as `8g`. Three quarters of the runner's memory if left out. |
285| `--cpus` | Each job container's CPUs, such as `4` or `1.5`. Every CPU if left out. |
286| `--pids-limit` | The most processes in a job container. 4096 if left out. |
287| `--no-job-docker` | Give jobs no [Docker of their own](#docker-in-jobs). |
288| `--job-docker-image` | The image a job's own Docker Engine runs. `docker:28-dind` if left out. |
289| `--work-dir` | Where jobs run with `--no-docker` keep their files. `~/.g1t-runner/work` if left out. |
290| `--dir` | Where the runner keeps its configuration and credential. `~/.g1t-runner`, or `G1T_RUNNER_DIR`, if left out. Every command takes it. |
291| `--replace` | Take the place of a runner with the same name. |
292| `--no-auto-update` | Never update itself. |
293
294To run several runners on one machine, give each its own `--dir` and
295`--name`.
296
297## Use it in a workflow
298
299A job runs on a self-hosted runner when its `runs-on` names `self-hosted`:
300
301```yaml
302jobs:
303 test:
304 runs-on: self-hosted
305 steps:
306 - uses: actions/checkout@v5
307 - run: make test
308
309 gpu:
310 runs-on: [self-hosted, linux, gpu]
311 steps:
312 - run: nvidia-smi
313
314 windows:
315 runs-on: [self-hosted, windows]
316 steps:
317 - run: Get-ComputerInfo | Select-Object OsName
318```
319
320Until a runner with every label the job names takes it, the job waits, and
321its page says what for: *Waiting for a self-hosted runner with labels
322self-hosted, linux, gpu.* A job that waits a day fails. Jobs that have
323waited ten minutes with no matching runner online show under **Needs you**
324on Mission control.
325
326`runs-on: { group: GPU, labels: [linux] }` asks for a runner in the group
327called GPU as well.
328
329### Labels
330
331Every runner has `self-hosted`, its OS (`linux`, `macos` or `windows`) and
332its architecture (`x64` or `arm64`), then the labels you gave it. A runner
333that runs jobs in Docker is `linux`, whatever the machine is, since that is
334what its jobs run on. Labels are matched without regard to case.
335
336A job takes a runner only when every label in its `runs-on` is one of the
337runner's. Labels that name g1t's own machines, such as `ubuntu-latest` or
338`g1t-4core`, never go to a self-hosted runner.
339
340### What a job gets
341
342A job on your runner runs exactly as it would in g1t's sandbox: the same
343harness, the same `${{ }}` contexts, `GITHUB_*` variables, secrets and
344workflow commands, the same log and artifacts. `runner.name`, `runner.os`
345and `runner.arch` are the machine's, and `RUNNER_ENVIRONMENT` is
346`self-hosted`.
347
348- **In a container** (the default), each job gets a fresh container from
349 its `container:` image or the runner's `--image`, removed when it ends,
350 with the walls in [what a job's container gets](#what-a-jobs-container-gets).
351 In the runner's image under Sysbox, its `services:` and Docker actions
352 get [a Docker Engine of its own](#docker-in-jobs).
353- **With `--no-docker`** (deprecated), each job gets a fresh folder under
354 the work folder, removed when it ends, and runs with whatever the
355 machine has installed. A step's default shell is `bash` on Linux and
356 macOS and PowerShell on Windows; `shell: pwsh`, `powershell`, `cmd`,
357 `bash` and `python` work where installed. On a machine with Docker, the
358 job's `services:`, `container:`, `docker://` steps and Docker actions
359 use the machine's Docker, as on GitHub's runners.
360
361A self-hosted job stops at 360 minutes unless its `timeout-minutes` says
362otherwise, as on GitHub, and can ask for up to 24 hours (1440). Jobs on
363g1t's own machines stop at 60 minutes.
364
365## Groups
366
367A workspace's runners are in groups, which say which repositories may use
368them. Every workspace has a **Default** group, for every repository, which
369runners join unless their token or `--group` names another.
370
371To keep a set of machines for some repositories, open the workspace's
372**Runners** page, click **New group**, name it, and choose **Only these**
373repositories. Then click **New runner** with that group chosen, or register
374with `--group`. Deleting a group moves its runners to the default group.
375
376## Agents on your runners
377
378**Runners → Where work runs** can send g1t's own work to your
379runners too: agent runs, checks, reviews, merge checks and the merge queue.
380Switch on **Run g1t's work on self-hosted runners** and give the labels
381a runner needs to take it (`self-hosted` is always one).
382
383- The agent works exactly as in g1t's sandbox: the same harness, with a
384 short-lived credential that can do only what that kind of run may, in
385 that repository, revoked when it ends.
386- Its model calls still go through g1t's model proxy, `models.g1t.sh`, with
387 that credential, so budgets, caps and the audit log work as before. With
388 your own [model provider](/guides/models/), g1t charges nothing for the
389 run; otherwise the model is paid as usual and the machine is free.
390- Agent work needs an image with git, Node, `tar` and the agent's CLI:
391 register the runner with `--agent-image` and an image of yours that has
392 them. Host mode on a Linux machine set aside for it, where `/work` can be
393 written, also works, and is deprecated. g1t does not publish an agent
394 image yet.
395- [Guardrails](/guides/guardrails/) still apply to what the agent does, but
396 their network list cannot be enforced on your machine. The run's session
397 says so when it starts.
398
399A project can have its own setting, or follow the workspace's.
400
401## What a run gets from Customize
402
403When g1t's work runs Claude Code on a runner (on g1t's own machines too),
404the job gets what the workspace set up in [Customize](/guides/customize/),
405as the job's agent would have it. Before Claude Code starts, the runner
406reads the workspace's bundle with the job's short-lived agent credential,
407once per job, and makes it ready:
408
409| In the bundle | What the runner does | In Claude Code |
410| --- | --- | --- |
411| 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 |
412| 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>` |
413| 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>` |
414
415The run's session gets one line saying what was loaded: *Loaded 2 plugins,
4161 library skill and 3 connectors from the workspace's Customize.* A plugin
417that could not be fetched, or a bundle that could not be read, is named
418there and the run goes on without it; nothing from Customize fails a run.
419Plugins from GitHub need `tar` on the machine.
420
421A run on a fork's head loads none of the fork's own settings, hooks, MCP
422servers or commands ([guardrails](/guides/guardrails/)). The workspace's
423plugins and connectors are the workspace's, not the checkout's, so they are
424still passed and merged; but every skill and command is switched off on
425such a run, the plugins' and the library's included, so an agent cannot
426invoke them there.
427
428The bundle carries the connectors' sign-ins, so only a job's agent
429credential or a workspace's token reads it, never a person's:
430[`GET /workspaces/{workspace}/customize/bundle`](/reference/api/customize/customize-bundle/),
431scope `agents:run`. g1t never keeps or logs it, and the runner keeps the
432keys it brought out of the session. See
433[what a run gets](/guides/customize/#what-a-run-gets).
434
435## Billing
436
437Time on your runners is free. Each job's minutes go on
438[usage](/guides/usage-and-billing/) as **Self-hosted runner time** at $0,
439so you can see how much ran there.
440
441- Workflow jobs on your runners need no plan and no card: they work on the
442 free tier.
443- Agent work on your runners still needs its model paid for: the g1t plan,
444 the trial, the open-source pool on a public repository, or your own
445 model provider, which makes the run free.
446
447## Security
448
449- **Pull requests from forks never run on your runners** unless you allow
450 it under **Runners → Where work runs**. Leave it off on a
451 public repository: anyone who can open a pull request could run any code
452 on the machine, read what it can reach, and leave something behind for
453 the next job. Such jobs get no secrets either way.
454- **Secrets** reach a job on your runner under the same rules as on g1t's:
455 only trusted runs get them, and only the ones for the job's environment.
456- **The runner's credential** can poll for work, report on what it was
457 given and remove itself, and nothing else; every other endpoint refuses
458 it. It is kept only as a hash on g1t's side, rotates every day, and stops
459 working the moment the runner is removed. It is never given to a job:
460 each job gets only its own short-lived token.
461- **Isolation**: run the runner's image under Sysbox. Each job then gets
462 a container of its own inside the runner's container, removed after,
463 and neither reaches the machine. A job with `--no-docker` runs as the
464 runner's user and can reach whatever that user can; host mode is
465 deprecated and refuses public repositories' work. Use ephemeral runners
466 for code you do not trust. See [isolation](#isolation).
467- **Network**: g1t's network guardrails are not enforced on your machines.
468 A job reaches whatever the machine can.
469- **Audit**: making a registration token, registering, removing, and every
470 job a runner takes are in the [audit log](/guides/audit-log/), with the
471 runner as the actor.
472- Only owners (or a repository's admins) register and remove runners,
473 signed in or with a person's token with `runners:admin`. Workspace
474 tokens, `G1T_TOKEN` included, cannot, so a workflow cannot add a machine
475 to run its own jobs.
476
477## Docker and Kubernetes
478
479[Run it as a container](#run-it-as-a-container) covers Docker. The image's
480settings, besides the register options:
481
482| Variable | |
483| --- | --- |
484| `G1T_RUNNER_SANDBOX` | `nested` in the image: start a Docker Engine inside the container and run jobs on it. |
485| `G1T_RUNNER_DIR` | `/data` in the image: where the configuration and credential are kept. |
486| `G1T_RUNNER_UNSAFE_ALLOW_HOST_ACCESS` | `1` starts the runner even when it can reach the machine. See [above](#run-it-as-a-container). |
487
488A runner that ran the image with the machine's Docker socket, as these
489docs said before, refuses to start with the new image. Remove the
490`-v /var/run/docker.sock:/var/run/docker.sock` and add
491`--runtime=sysbox-runc`; the `/data` volume keeps its registration.
492
493### Autoscaling
494
495Ephemeral runners take one job and remove themselves, so each job starts in
496a fresh runner container. In Kubernetes with Sysbox installed on the
497nodes (its Kubernetes install adds the `sysbox-runc` runtime class), run
498them in pods that make their own registration token from an owner's
499personal [access token](/guides/authentication/#access-tokens) with only
500`runners:admin`, and let the Deployment replace each pod that finishes:
501
502```yaml
503apiVersion: apps/v1
504kind: Deployment
505metadata:
506 name: g1t-runner
507spec:
508 replicas: 3
509 selector: { matchLabels: { app: g1t-runner } }
510 template:
511 metadata:
512 labels: { app: g1t-runner }
513 annotations: { io.kubernetes.cri-o.userns-mode: "auto:size=65536" }
514 spec:
515 runtimeClassName: sysbox-runc
516 containers:
517 - name: runner
518 image: g1t.sh/flagon-io/g1t-runner
519 command: ["sh", "-c"]
520 args:
521 - |
522 export G1T_RUNNER_TOKEN="$(curl -fsS -X POST \
523 -H "Authorization: Bearer $G1T_ADMIN_TOKEN" \
524 https://api.g1t.sh/workspaces/acme/actions/runners/registration-token \
525 | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
526 exec g1t-runner register-and-run
527 env:
528 - { name: G1T_RUNNER_URL, value: https://g1t.sh }
529 - { name: G1T_RUNNER_LABELS, value: k8s }
530 - { name: G1T_RUNNER_EPHEMERAL, value: "1" }
531 - name: G1T_ADMIN_TOKEN
532 valueFrom: { secretKeyRef: { name: g1t, key: runners-admin-token } }
533```
534
535To scale with demand rather than keep a fixed number, change `replicas`
536with your autoscaler of choice: [`list_runners`](/reference/api/runners/list-runners-for-workspace/)
537says which are busy.
538
539## Updates
540
541The runner checks for a new release every six hours while it is idle, and
542updates itself when the release is signed by g1t's release key and its
543download matches the release's SHA-256. `g1t-runner update` does it now;
544`--no-auto-update` turns it off. Releases are at
545`https://g1t.sh/downloads/runner/<version>/`, with a `SHA256SUMS` file.
546
547## Remove a runner
548
549On the machine, `g1t-runner remove` unregisters it and forgets its
550credential (run `service uninstall` first if it is a service). Or remove it
551from the **Runners** page; the runner stops on its next poll. A job it was
552running fails. Runners offline for 14 days are removed by themselves.
553
554## API and MCP
555
556| What | REST | MCP (`workflow` tool) | Scope |
557| --- | --- | --- | --- |
558| 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` |
559| Make a registration token | [`POST …/actions/runners/registration-token`](/reference/api/runners/create-runner-registration-token-for-workspace/) | `create_runner_token` | `runners:admin` |
560| Remove a runner | [`DELETE …/actions/runners/{id}`](/reference/api/runners/remove-runner-for-workspace/) | `remove_runner` | `runners:admin` |
561| 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` |
562| 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` |
563| 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 |
564
565The [Agent preset](/guides/authentication/#scopes) does not include
566`runners:read`.
567
568## Troubleshooting
569
570| What you see | What to do |
571| --- | --- |
572| 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. |
573| *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. |
574| `register` says the token is not valid | It expired after an hour, or was mistyped. Make a new one. |
575| `register` says a runner with that name is registered | Choose another `--name`, or add `--replace`. |
576| `run` says *g1t no longer knows this runner* | It was removed, or its credential was. Register it again. |
577| *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). |
578| *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). |
579| 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`. |
580| *… 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. |
581| 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`. |
582| *could not run docker* | Install Docker and start it, and give the runner's user access to it. |
583| Agent work fails at once | Give the runner `--agent-image`. |
584| 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`. |