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