Add runner 2 and 3 documentations (#488)

Reviewed-on: https://gitea.com/gitea/docs/pulls/488
Reviewed-by: silverwind <[email protected]>
This commit is contained in:
Lunny Xiao
2026-08-07 21:02:26 +00:00
parent 7629c02a9a
commit 0c62ec4893
57 changed files with 5215 additions and 37 deletions
@@ -0,0 +1,96 @@
---
sidebar_position: 5
---
# Caching
Every runner starts its own cache server, so `actions/cache` works without any configuration. Cache entries are local to that runner: two runners do not share a cache unless you make them.
Only the cache service v1 API is served in this version, so `actions/cache` has to be pinned to a version that still speaks it (up to `v4.1`), and artifacts need the `gitea-upload-artifact` / `gitea-download-artifact` forks. Runner `3.0` adds cache service v2, which the stock actions use.
## Where cache blobs are stored
```yaml
cache:
enabled: true
dir: /var/lib/gitea-runner/cache # default: $HOME/.cache/actcache
```
The directory grows with use; entries are evicted as they expire, so give it a filesystem with room to spare and monitor it like any other build cache.
## Dockerized runners
When the runner itself runs in a container and creates a network per job, the address it detects for its own cache server is often unreachable from the job containers. `actions/cache` then fails with:
```text
Failed to restore: getCacheEntry failed: connect ETIMEDOUT IP:PORT
```
Pin the address and the port the job containers should use, and make that endpoint reachable:
1. take an address of the host that job containers can reach, and a free port on it;
2. configure them:
```yaml
cache:
enabled: true
dir: ""
host: "192.168.8.17"
port: 8088
```
3. publish the port when starting the runner container:
```bash
docker run -d --name gitea-runner -p 8088:8088 ... docker.io/gitea/runner:2
```
Putting the runner and the job containers on one shared `container.network` instead works too, and then the auto-detected address is reachable.
## Sharing a cache between runners
Run one dedicated cache server that every runner points at.
1. Config for the cache server host:
```yaml
cache:
dir: /data/actcache
port: 8088
external_secret: "replace-with-a-strong-random-secret"
# external_secret_file: /run/secrets/cache-secret # or keep it out of this file
```
2. Start it:
```bash
gitea-runner -c cache-server-config.yaml cache-server
```
3. On every runner:
```yaml
cache:
external_server: "http://cache-host:8088/"
external_secret: "replace-with-a-strong-random-secret" # must match the server
```
The secret authenticates runners against the shared server and must be identical on all of them; generate one with `openssl rand -hex 32`. Setting both `external_secret` and `external_secret_file` is an error.
`cache-server` accepts `--dir`, `--host` and `--port`, which override the corresponding `cache.*` keys. Every other setting, `external_secret` included, has to come from the config file.
### Alternatives
- **Shared filesystem** — mount the same NFS/CIFS share on every runner and point `cache.dir` at it. Simpler, but repositories are less isolated from each other than behind a cache server.
- **Object storage** — mount S3 or MinIO as a FUSE filesystem, e.g. with [s3fs](https://github.com/s3fs-fuse/s3fs-fuse) or [goofys](https://github.com/kahing/goofys), and set `cache.dir` to the mount point.
## Action repository cache
Actions pulled by `uses:` are cached too, and by default refreshed on every job so a moved tag is picked up. To pin them to what has already been fetched:
```yaml
cache:
offline_mode: true
```
A re-tagged `v6` or an updated branch then stays at the cached commit until its entry expires or is removed. Combined with `runner.action_shallow_clone` (on by default, fetching only the requested ref at depth 1), this keeps job startup fast on runners with limited bandwidth.
@@ -0,0 +1,137 @@
---
sidebar_position: 3
---
# Configuration
The runner is configured with a single YAML file. It is optional: without one, the built-in defaults apply, which are the same as an empty YAML document and safe to run with.
```bash
gitea-runner generate-config > config.yaml
gitea-runner -c config.yaml register
gitea-runner -c config.yaml daemon
```
`-c` / `--config` is a global flag and is accepted by every command that loads configuration (`register`, `daemon`, `cache-server`). The generated file is fully commented and is reproduced in [Example configuration](reference/config-example.md).
:::warning No environment variable overrides
The runner process is configured only through the YAML file. Earlier releases let a few variables (`GITEA_DEBUG`, `GITEA_TRACE`, `GITEA_RUNNER_CAPACITY`, `GITEA_RUNNER_FILE`, `GITEA_RUNNER_ENVIRON`, `GITEA_RUNNER_ENV_FILE`) override parts of the config; those overrides have been removed.
The variables understood by the Docker images belong to their [entrypoint](installation/docker.md#entrypoint-environment-variables), not to the runner, and `GITEA_RUNNER_LABELS` / `GITEA_RUNNER_REGISTRATION_TOKEN` are read by the corresponding CLI flags only.
:::
Values with a duration type accept Go duration strings such as `30s`, `10m`, `3h`.
## `log`
Controls the runner's own log, not how step output is streamed to the UI.
| Option | Default | Description |
| --- | --- | --- |
| `log.level` | `info` | `trace`, `debug`, `info`, `warn`, `error`, `fatal` or `panic`. `trace` and `debug` add the caller's `file:line`. |
## `runner`
| Option | Default | Description |
| --- | --- | --- |
| `file` | `.runner` | path of the registration file. Each runner process needs its own. |
| `capacity` | `1` | jobs executed concurrently. With an empty `container.network`, every concurrent docker job takes a subnet from the daemon's address pool, so a high capacity can exhaust it (see `default-address-pools` in the daemon config). |
| `envs` | | extra environment variables given to every job. |
| `env_file` | `.env` | same, read from a file; ignored when empty or missing. |
| `timeout` | `3h` | maximum job duration. Gitea has its own timeout (3h by default) and may stop the job earlier. |
| `shutdown_timeout` | `0s` | how long a shutdown waits for running jobs before cancelling them. |
| `insecure` | `false` | skip TLS verification of the Gitea instance. |
| `fetch_timeout` | `5s` | timeout of a single job fetch. |
| `fetch_interval` | `2s` | base polling interval. |
| `fetch_interval_max` | `5s` | upper bound of the exponential backoff applied while idle. `0`, or the same value as `fetch_interval`, disables the backoff. |
| `labels` | see [Labels](labels.md) | labels used at registration, and by `daemon` when the flag is absent. |
| `github_mirror` | | replaces `https://github.com` when actions are pulled and the instance's `DEFAULT_ACTIONS_URL` points at GitHub. |
| `action_shallow_clone` | `true` | fetch only the requested ref of an action repository at depth 1 instead of its full history. |
| `set_act_env` | `true` | inject `ACT=true` into jobs. Set to `false` so workflows gated on `if: ${{ !env.ACT }}` behave as they do on GitHub. |
| `allocate_pty` | `false` | allocate a pseudo-TTY per step. Enable only when a job needs an interactive terminal; tools like `docker build` then write redrawing progress frames into the log. |
| `workdir_cleanup_age` | `24h` | age at which stale task workspaces and orphaned host-mode scratch directories are removed while idle. |
| `idle_cleanup_interval` | `10m` | cadence of the idle cleanup pass. Setting either this or `workdir_cleanup_age` to `0` disables all idle cleanup. |
| `post_task_script` | | host script run after each task's cleanup, see [Post-task script](hooks/post-task-script.md). |
| `post_task_script_timeout` | `5m` | hard limit for that script. |
Log and state reporting can be tuned when the UI updates too slowly or the instance sees too many requests:
| Option | Default | Description |
| --- | --- | --- |
| `log_report_interval` | `5s` | base interval of the periodic log flush. |
| `log_report_max_latency` | `3s` | maximum time a single log row waits. Only has an effect below `log_report_interval`. |
| `log_report_batch_size` | `100` | flush immediately once this many rows are buffered, so bursty output arrives promptly. |
| `state_report_interval` | `5s` | interval of task state reports. State is also sent on every step transition. |
| `report_close_timeout` | `10s` | per-attempt deadline for the final log and state flush of a finished job. |
### Idle cleanup
While no job is running, the runner cleans up after earlier ones:
- stale task workspaces older than `workdir_cleanup_age` are removed when `container.bind_workdir` is enabled. Only purely numeric subdirectories of `container.workdir_parent` are treated as workspaces, and the path is assumed not to be shared with another runner;
- orphaned host-mode scratch directories are removed on the same schedule.
Per-job docker networks of jobs the runner did not live to tear down are *not* cleaned up in this version, and each of them keeps holding a subnet of the daemon's address pool until it is removed manually. Runner `3.0` removes them as part of the idle pass.
## `cache`
See [Caching](cache.md) for the full picture, including shared cache servers.
| Option | Default | Description |
| --- | --- | --- |
| `enabled` | `true` | run the built-in cache server used by `actions/cache` and friends. |
| `dir` | `$HOME/.cache/actcache` | where cache blobs are stored. Ignored with `external_server`. |
| `host` | | address job containers use to reach this runner's cache server. Empty means auto-detect; `0.0.0.0` is not valid. |
| `port` | `0` | port of the built-in server, `0` picks a free one. |
| `external_server` | | URL of a shared `cache-server` to use instead of a local one. It has to end with `/`. |
| `external_secret` | | shared secret, required with `external_server`; must be identical everywhere. Generate with `openssl rand -hex 32`. |
| `external_secret_file` | | read that secret from a file instead. Setting both is an error. |
| `offline_mode` | `false` | reuse a cached action instead of fetching it on every job. A moved tag or updated branch then stays at the cached commit until the entry expires or is removed. |
## `container`
Applies to jobs that run in containers.
| Option | Default | Description |
| --- | --- | --- |
| `network` | | network the job container joins: `host`, `bridge`, or a custom network name. Empty means the runner creates one per job. `network_mode` is still accepted for old configs. |
| `network_create_options.enable_ipv4` / `enable_ipv6` | Docker defaults | only apply to auto-created networks. IPv6 additionally requires `dockerd --ipv6`. |
| `privileged` | `false` | run job containers privileged; required for Docker-in-Docker inside jobs. |
| `options` | | extra `docker run` options, e.g. `--add-host=my.gitea.url:host-gateway`, `--platform` or `--pull`. |
| `workdir_parent` | `/workspace` | parent directory of a job's working directory inside the container. A leading `/` is trimmed and re-added. |
| `valid_volumes` | `[]` | volumes and bind mounts a job may mount, as [glob](https://github.com/gobwas/glob) patterns. `[]` forbids all, `['**']` allows all. |
| `docker_host` | | override the docker host. Empty auto-detects it, `-` auto-detects it but does not mount the socket into job containers. |
| `force_pull` | `false` | pull images even when present. |
| `force_rebuild` | `false` | rebuild local action images even when present. |
| `require_docker` | `false` | always require a reachable daemon, even for host-only labels. |
| `docker_timeout` | `0s` | how long to wait for the daemon to become reachable. |
| `bind_workdir` | `false` | bind-mount the workspace from the host instead of using a docker volume. Needed for jobs that use `docker compose` with bind mounts under Docker-in-Docker. The parent directory must then be mounted into the runner container and listed in `valid_volumes`. |
:::warning Workflow container options are not filtered
A workflow's own `jobs.<job_id>.container.options` are merged into the container configuration as they are, apart from `--privileged` itself. A workflow can therefore reach the host through options such as `--pid=host` or `--cap-add=ALL`. Runner `3.0` strips those options while `container.privileged` is disabled; until then, only grant this runner to repositories you trust.
:::
## `host`
| Option | Default | Description |
| --- | --- | --- |
| `host.workdir_parent` | `$HOME/.cache/act/` | parent directory of a job's working directory for host-mode jobs. |
## `health_check` and `metrics`
Both are covered in [Monitoring](monitoring.md).
| Option | Default | Description |
| --- | --- | --- |
| `health_check.enabled` | `false` | pause fetching new tasks while the machine looks unhealthy. |
| `health_check.min_free_disk_space_mb` | `1024` | minimum free space on the filesystem holding the workspaces. |
| `health_check.script` | | extra executable; a non-zero exit, a timeout or a start failure marks the runner unavailable. |
| `health_check.interval` | `30s` | how long a result is cached. |
| `health_check.timeout` | `10s` | maximum script runtime. |
| `metrics.enabled` | `false` | serve `/metrics`, `/healthz` and `/readyz`. |
| `metrics.addr` | `127.0.0.1:9101` | listen address. There is no authentication, so only expose it behind a firewall. |
| `metrics.readiness_grace` | `30s` | how long consecutive polling failures may last before `/readyz` returns 503. |
## Reloading
The runner reads its configuration at startup only. Restart the process after a change — with `shutdown_timeout` set, running jobs are given that much time to finish first.
@@ -0,0 +1,100 @@
---
sidebar_position: 2
---
# Post-task script
The post-task script is an optional host hook that runs **once after every task**, after the runner has finished its normal per-task cleanup. Typical uses are pruning Docker images, vacuuming ephemeral disks, or resetting VM state between jobs.
```yaml
runner:
# Path to an executable on the host. Empty or omitted disables the hook.
post_task_script: /usr/local/bin/gitea-post-task.sh
# Hard limit on script runtime. Default when post_task_script is set: 5m.
post_task_script_timeout: 2m
```
| Option | Default | Description |
| --- | --- | --- |
| `runner.post_task_script` | disabled | host path to the script or binary. Relative paths are resolved from the runner's working directory. |
| `runner.post_task_script_timeout` | `5m` when a script is set | maximum runtime before the runner kills the script and moves on. |
## When it runs
For each task, the order is:
1. the workflow runs (steps, actions, containers);
2. in-job cleanup (action `post:` steps, container stop and removal);
3. job outputs are reported to Gitea;
4. the bind-workdir workspace is removed, when `container.bind_workdir` is enabled;
5. **the post-task script**;
6. the final task acknowledgement to Gitea.
The script is **additive**: it does not replace any built-in cleanup. With `container.bind_workdir` enabled, the workspace directory has usually already been deleted before the script starts, but `GITEA_WORKSPACE` still names the path the job used.
## The runner stays offline until the script finishes
This is the most important operational detail. When the script starts, the runner **stops sending task heartbeats**, so from Gitea's perspective it is not available for new work until the script exits and the final task flush has been sent.
While the script runs:
- Gitea does not assign another task to this runner for the current job slot;
- the capacity slot stays occupied locally — with `capacity: 1`, no other task starts;
- a shutdown counts this phase as part of the in-flight task, so a slow script delays graceful shutdown.
If the script never exits, the runner stays in this state until `runner.post_task_script_timeout` elapses (default **5 minutes**), then kills it and proceeds. Set that timeout to what your housekeeping is allowed to take, and keep the script short and bounded.
## Environment variables
The script receives `runner.envs` / `runner.env_file` values plus:
| Variable | Description |
| --- | --- |
| `GITEA_TASK_ID` | numeric task ID |
| `GITEA_RUN_ID` | workflow run ID, when the server provides it |
| `GITEA_REPOSITORY` | repository slug (`owner/name`) |
| `GITEA_WORKSPACE` | workspace path the job used, which may already be deleted |
| `GITEA_JOB_RESULT` | `success`, `failure`, `cancelled`, `skipped` or `unknown` |
The environment is **not** a copy of the job container's. Even `PATH` is only present if `runner.envs` or `runner.env_file` defines it.
## Output and errors
- stdout and stderr go to the **runner process log**, prefixed with `post-task script stdout:` / `post-task script stderr:` — not to the job log;
- a non-zero exit is logged as a warning and does not change the job result already reported to Gitea;
- timeouts and start failures are warnings too; the runner still acknowledges the task.
## Interaction with other timeouts
| Timeout | Effect on the post-task script |
| --- | --- |
| `runner.post_task_script_timeout` | kills the script if it runs too long. The **only** timeout that bounds it. |
| `runner.timeout` | caps the task **up to** the script. The script detaches from the task deadline, so a job that nearly hit the runner timeout does not cut it short. |
| `runner.shutdown_timeout` | bounds how long a shutdown waits for the **task**. The script detaches from cancellation and may extend shutdown until its own timeout elapses. |
## Examples
Prune dangling Docker resources on Linux:
```sh
#!/bin/sh
set -eu
docker image prune -f
docker builder prune -f --filter 'until=24h'
```
On Windows, use a `.exe`, `.bat` or `.cmd` path; `.ps1` is not supported as the configured path, so wrap PowerShell in a batch file:
```bat
@echo off
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0post-task.ps1"
```
`.sh` files on Windows need a Unix shell on `PATH`, unless `post_task_script` points at the interpreter itself.
## Notes
- `gitea-runner exec` does not load the runner YAML and never runs this hook.
- Use idempotent operations: the script runs after success, failure and cancellation alike.
- Watch the runner log when testing failure modes — a hung script, a non-zero exit, a missing executable.
- Bind-workdir idle cleanup (`runner.workdir_cleanup_age`) is separate from this hook and only runs while the runner is idle.
@@ -0,0 +1,49 @@
---
sidebar_position: 1
slug: /
---
# Gitea Runner
The [Gitea Runner](https://gitea.com/gitea/runner) executes the jobs of [Gitea Actions](/usage/actions/overview).
It polls a Gitea instance for queued jobs, runs their steps in a container or directly on the machine it is installed on, and streams the logs and the result back.
:::info
These pages describe the runner `2.x` series. Pick another release, or the development version, in the **Runner Version** dropdown.
:::
## Requirements
A runner needs a Gitea instance with Actions enabled, a [registration token](registration.md), and, for containerized jobs, a Docker daemon. Actions are enabled by default since Gitea 1.21; on older instances they have to be turned on:
```ini
[actions]
ENABLED=true
```
Other OCI engines that implement the Docker API may work, but are untested. Podman is not a supported configuration.
## Execution modes
A runner can run jobs in three different ways. The mode is not a global setting: it follows from the [labels](labels.md) the runner is registered with, so a single runner can offer both container and host labels.
| Mode | How jobs run | Docker daemon | Notes |
| --- | --- | --- | --- |
| Docker (recommended) | in a container created from the label's image | external, e.g. the host's `/var/run/docker.sock` | jobs are isolated from each other, but share the daemon |
| Docker-in-Docker | in a container created by a daemon that lives next to the runner | bundled in the `dind` / `dind-rootless` images | strongest isolation, more setup, needs `--privileged` |
| Host | directly on the machine, with the tools installed there | only needed for `docker://` actions and service containers | no isolation between jobs |
## Getting started
1. [Install the runner](installation/binary.md) as a binary, [in Docker](installation/docker.md), or [on Kubernetes](installation/kubernetes.md).
2. [Register it](registration.md) against your instance with a registration token.
3. [Configure it](configuration.md), and pick the [labels](labels.md) that decide which jobs it accepts.
4. Optionally set up a [shared cache](cache.md), a [post-task script](hooks/post-task-script.md), or [metrics and health checks](monitoring.md).
Every command and flag is listed in the [command line reference](reference/cli.md).
## Versioning and compatibility
The runner is released independently of Gitea and its version numbers are unrelated to the instance's. Gitea 1.21 or later is expected — older instances cannot accept the runner's label declaration — and individual features need a newer instance still, which is called out where they apply.
When moving between major runner versions, read [Upgrading](upgrade.md) first: `2.0.0` contains breaking changes.
@@ -0,0 +1,151 @@
---
sidebar_position: 1
---
# Install from a binary
The runner is a single static binary called `gitea-runner`. It has no dependencies apart from a Docker daemon for containerized jobs.
## Download
- released builds: [dl.gitea.com/gitea-runner](https://dl.gitea.com/gitea-runner/) or the [release page](https://gitea.com/gitea/runner/releases)
- development builds of the `main` branch: [dl.gitea.com/gitea-runner/nightly](https://dl.gitea.com/gitea-runner/nightly/)
Each file is published next to a `.sha256` checksum and an `.xz` compressed variant:
```bash
VERSION=2.3.0 # any 2.x release, see the release page
curl -sSLO "https://dl.gitea.com/gitea-runner/$VERSION/gitea-runner-$VERSION-linux-amd64"
curl -sSLO "https://dl.gitea.com/gitea-runner/$VERSION/gitea-runner-$VERSION-linux-amd64.sha256"
sha256sum -c "gitea-runner-$VERSION-linux-amd64.sha256"
install -m 0755 "gitea-runner-$VERSION-linux-amd64" /usr/local/bin/gitea-runner
```
Check that the binary matches your platform:
```bash
gitea-runner --version
```
## Build from source
Building requires the Go version declared in the repository's `go.mod`:
```bash
git clone https://gitea.com/gitea/runner.git
cd runner
make build
```
## First run
```bash
gitea-runner generate-config > config.yaml # optional, defaults are safe
gitea-runner -c config.yaml register # see "Registering a runner"
gitea-runner -c config.yaml daemon
```
The `daemon` command runs in the foreground. It reads the registration file (`runner.file`, `.runner` by default) relative to its working directory, so keep the working directory stable across restarts.
## Run as a systemd service
Create an unprivileged user, install the binary, and register the runner as that user so the `.runner` file ends up in the service's working directory:
```bash
sudo useradd --system --home-dir /var/lib/gitea-runner --create-home gitea-runner
sudo install -d /etc/gitea-runner
sudo -u gitea-runner gitea-runner generate-config | sudo tee /etc/gitea-runner/config.yaml >/dev/null
cd /var/lib/gitea-runner
sudo -u gitea-runner gitea-runner register -c /etc/gitea-runner/config.yaml
```
Then install the unit as `/etc/systemd/system/gitea-runner.service`:
```ini
[Unit]
Description=Gitea Actions runner
Documentation=https://gitea.com/gitea/runner
After=network-online.target
Wants=network-online.target
# Uncomment when jobs use the local Docker daemon:
# After=docker.service
# Requires=docker.service
[Service]
Type=simple
ExecStart=/usr/local/bin/gitea-runner daemon --config /etc/gitea-runner/config.yaml
WorkingDirectory=/var/lib/gitea-runner
User=gitea-runner
Group=gitea-runner
Restart=on-failure
RestartSec=5s
# Allow running jobs to finish before the runner is stopped. Keep this in sync
# with runner.shutdown_timeout in the config.
TimeoutStopSec=3h
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now gitea-runner
```
If jobs use the host's Docker daemon, the `gitea-runner` user also needs access to the daemon socket. Adding it to the `docker` group grants that access and is [equivalent to root on the host](https://docs.docker.com/engine/security/#docker-daemon-attack-surface).
## Run as a launchd daemon (macOS)
macOS uses `launchd` instead of systemd. Daemons run as `root` by default; an unprivileged `_gitea-runner` user can be created with `dscl`. Install the following as `/Library/LaunchDaemons/com.gitea.runner.plist` and adjust the paths to your installation:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.gitea.runner</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/gitea-runner</string>
<string>daemon</string>
<string>--config</string>
<string>/etc/gitea-runner/config.yaml</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>WorkingDirectory</key>
<string>/var/lib/gitea-runner</string>
<key>StandardOutPath</key>
<string>/var/lib/gitea-runner/runner.log</string>
<key>StandardErrorPath</key>
<string>/var/lib/gitea-runner/runner.err</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>HOME</key>
<string>/var/lib/gitea-runner</string>
</dict>
<key>UserName</key>
<string>_gitea-runner</string>
</dict>
</plist>
```
```bash
sudo launchctl load /Library/LaunchDaemons/com.gitea.runner.plist
```
On macOS and Windows hosts, jobs usually run with [host labels](../labels.md) and the tools installed on the machine.
## Windows
Install the `windows-amd64` binary and register it as a service with any service wrapper (for example `sc.exe` plus a wrapper such as [WinSW](https://github.com/winsw/winsw), or a scheduled task at boot). The runner itself has no service-installer subcommand.
Keep in mind for Windows hosts:
- `runner.post_task_script` accepts `.exe`, `.bat` and `.cmd` paths; `.ps1` is not supported as the configured path.
- host-mode jobs are terminated as a process tree, so tools that daemonize are not left behind.
@@ -0,0 +1,147 @@
---
sidebar_position: 2
---
# Install with Docker
The official images are published on [Docker Hub](https://hub.docker.com/r/gitea/runner/tags) as `docker.io/gitea/runner`.
Every release is tagged with its version, and the `2` tag follows the newest `2.x` release. `latest` points at the newest release of *any* series, so pin `2` (or an exact version) to stay on this one.
In the container the registration and the daemon are combined: the entrypoint registers the runner on first start (when no registration file exists yet) and then execs `gitea-runner daemon`.
## Image flavours
All flavours contain the same `gitea-runner` binary and differ only in how a Docker daemon is made available to jobs.
| Tag | Base image | Docker daemon | Supervisor | Runs as |
| --- | --- | --- | --- | --- |
| `2`, `2.3`, `<version>` | `alpine` | none, you provide one | `tini` | `root` |
| `2-dind`, `2.3-dind` | `docker:dind` | bundled, needs `--privileged` | `s6` | `root` |
| `2-dind-rootless`, `2.3-dind-rootless` | `docker:dind-rootless` | bundled, rootless | `s6` | `rootless` (UID 1000) |
The rootless flavour's UID is fixed at 1000 by the upstream base image, and its daemon always listens on `/run/user/1000/docker.sock`, so `--user 1001` does not work. To talk to a *host* rootless daemon under another UID, use the basic flavour and bind-mount that daemon's socket instead.
## Basic flavour
The default image ships no daemon of its own, so jobs that use `docker://` images need one from outside the container — usually the host's socket:
```bash
docker run -d --name my_runner \
-e GITEA_INSTANCE_URL=<instance_url> \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<registration_token> \
-e GITEA_RUNNER_NAME=<runner_name> \
-v $PWD/data:/data \
-v /var/run/docker.sock:/var/run/docker.sock \
docker.io/gitea/runner:2
```
This flavour does not need `--privileged`. The trade-off is that jobs share the host's daemon and can therefore see its other containers and images. A job that can reach the socket can also read the reusable `GITEA_RUNNER_REGISTRATION_TOKEN` from the runner container's `docker inspect` output.
## Docker-in-Docker
The `dind` flavour bundles its own daemon, so no socket has to be mounted:
```bash
docker run -d --name my_runner --privileged \
-e GITEA_INSTANCE_URL=<instance_url> \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<registration_token> \
-v $PWD/data:/data \
docker.io/gitea/runner:2-dind
```
`s6` starts `dockerd` first and the runner service waits for it before registering. Use `2-dind-rootless` to run both the daemon and the runner as an unprivileged user; rootless Docker's usual limitations around networking, cgroups and storage drivers apply.
## Volumes
Two different pieces of state are worth persisting, and neither implies the other:
- `/data` is the runner's working directory. It holds the `.runner` registration file and, optionally, the config file. Without it, a recreated container registers itself again as a new runner, leaving a stale entry in Gitea, and fails outright if the token has been reset in the meantime.
- the Docker daemon's data root holds the images pulled for jobs. It is **not** under `/data`: for `dind` it is `/var/lib/docker` inside the container, for `dind-rootless` it is `/home/rootless/.local/share/docker`. Give it its own volume, or every new container re-pulls the job images.
## Entrypoint environment variables
The entrypoint ([`scripts/run.sh`](https://gitea.com/gitea/runner/src/branch/main/scripts/run.sh)) understands:
| Variable | Meaning |
| --- | --- |
| `GITEA_INSTANCE_URL` | instance to register against, e.g. `https://gitea.example.com/` |
| `GITEA_RUNNER_REGISTRATION_TOKEN` | registration token; unset before the daemon starts |
| `GITEA_RUNNER_REGISTRATION_TOKEN_FILE` | file to read the token from, for Docker/Kubernetes secrets |
| `GITEA_RUNNER_NAME` | runner name, defaults to the container hostname |
| `GITEA_RUNNER_LABELS` | labels, passed to both `register` and `daemon` |
| `GITEA_RUNNER_EPHEMERAL` | any non-empty value registers the runner as [ephemeral](../registration.md#ephemeral-runners) |
| `GITEA_RUNNER_ONCE` | any non-empty value runs a single job, then exits |
| `GITEA_MAX_REG_ATTEMPTS` | registration attempts before giving up, default `10` |
| `RUNNER_STATE_FILE` | registration file name inside `/data`, default `.runner` |
| `CONFIG_FILE` | config file inside the container, passed as `--config` |
These are entrypoint variables, not runner settings: the runner process itself is configured only through the [config file](../configuration.md).
Mount the config file when you need one:
```bash
docker run -v $PWD/config.yaml:/config.yaml -e CONFIG_FILE=/config.yaml ...
```
A config file can be generated with the image itself:
```bash
docker run --rm --entrypoint="" docker.io/gitea/runner:2 gitea-runner generate-config > config.yaml
```
## docker compose
```yaml
services:
runner:
image: docker.io/gitea/runner:2
restart: always
environment:
CONFIG_FILE: /config.yaml
GITEA_INSTANCE_URL: "${INSTANCE_URL}"
GITEA_RUNNER_REGISTRATION_TOKEN: "${REGISTRATION_TOKEN}"
GITEA_RUNNER_NAME: "${RUNNER_NAME}"
GITEA_RUNNER_LABELS: "${RUNNER_LABELS}"
volumes:
- ./config.yaml:/config.yaml
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock
```
When Gitea runs in the same compose project, depend on its health check so the runner does not try to register before the instance answers:
```yaml
depends_on:
gitea:
condition: service_healthy
restart: true
```
The rootless Docker-in-Docker variant needs a few extra options:
```yaml
services:
runner:
image: docker.io/gitea/runner:2-dind-rootless
restart: always
privileged: true
security_opt:
# for hosts running AppArmor (Ubuntu, Debian), whose default profile blocks
# the user namespace changes the bundled daemon needs
- apparmor=rootlesskit
volumes:
- ./data/runner:/data
environment:
- GITEA_INSTANCE_URL=<instance_url>
- GITEA_RUNNER_REGISTRATION_TOKEN=<registration_token>
- DOCKER_HOST=unix:///var/run/user/1000/docker.sock
# slirp4netns gives significantly better network throughput than vpnkit
- DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns
- DOCKERD_ROOTLESS_ROOTLESSKIT_MTU=65520
```
## Cache from a dockerized runner
A runner in a container creates a separate network per job by default, so the address it detects for its own cache server is often unreachable from job containers and `actions/cache` fails with a connection timeout. Set `cache.host` and `cache.port` explicitly and publish that port, or put the job containers on a shared network — see [Caching](../cache.md#dockerized-runners).
More deployment examples live in the [`examples`](https://gitea.com/gitea/runner/src/branch/main/examples) directory of the runner repository.
@@ -0,0 +1,49 @@
---
sidebar_position: 3
---
# Install on Kubernetes
Ready-to-adapt manifests live in [`examples/kubernetes`](https://gitea.com/gitea/runner/src/branch/main/examples/kubernetes) of the runner repository, and a Helm chart is maintained at [gitea/helm-actions](https://gitea.com/gitea/helm-actions).
## Choosing a manifest
| Example | Shape | Docker daemon |
| --- | --- | --- |
| `dind-docker.yaml` | `Deployment` with a native sidecar (`initContainer` with `restartPolicy: Always`, needs Kubernetes 1.29+) | privileged `docker:dind` sidecar, socket shared through an `emptyDir` |
| `statefulset-dind.yaml` | `StatefulSet` with `volumeClaimTemplates` | same as above |
| `rootless-docker.yaml` | `Deployment` with a single container | bundled rootless daemon of the `dind-rootless` image, reached over `tcp://localhost:2376` with TLS |
Prefer the `StatefulSet` variant when you scale past one replica: each pod then gets a stable identity and its own volume, so it keeps its `.runner` registration across restarts and reschedules instead of registering itself again as a new runner.
## Two volumes, two purposes
- `/data` — the runner's working directory, holding the `.runner` registration file and optionally the config file.
- the daemon's data root — `/var/lib/docker` for the `dind` sidecar, `/home/rootless/.local/share/docker` for `dind-rootless`. It holds the images pulled for jobs and is *not* under `/data`. Dropping it still works, but every recreated pod re-pulls all job images.
With the rootless image, both volumes must be writable by UID/GID 1000, which is what `securityContext.fsGroup: 1000` in the example is for.
## Registration token
The examples read the token from a `Secret`:
```yaml
env:
- name: GITEA_INSTANCE_URL
value: http://gitea-http.gitea.svc.cluster.local:3000
- name: GITEA_RUNNER_REGISTRATION_TOKEN
valueFrom:
secretKeyRef:
name: runner-secret
key: token
```
The token stays valid for further registrations until it is reset, but each registration creates another runner entry. For pods that are recreated without their volume, use the instance-wide token configured on the Gitea side, and expect stale runner entries — or start an [ephemeral runner](../registration.md#ephemeral-runners) per job.
## Privileges
Docker-in-Docker needs `securityContext.privileged: true`, which lets a malicious job break out of the container. Weigh that against the alternatives:
- the rootless flavour, which reduces but does not remove the exposure;
- pointing the basic flavour at a daemon outside the cluster;
- keeping such runners on a dedicated node pool and only granting them to trusted repositories.
@@ -0,0 +1,70 @@
---
sidebar_position: 4
---
# Labels
Labels decide **which jobs a runner accepts** and **how it runs them**. A job's `runs-on` value is matched against the runner's label names; the first match wins and selects the execution environment for that job.
A label is written as:
```text
<name>[:<schema>[:<args>]]
```
| Part | Meaning |
| --- | --- |
| `name` | the name a workflow refers to in `runs-on`, e.g. `ubuntu-latest` |
| `schema` | either `docker` or `host`, defaulting to `host` when omitted |
| `args` | only used by the `docker` schema: the image the job runs in |
Two schemas are supported:
- `docker://<image>` — the job runs in a container created from `<image>`:
```text
ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest
```
- `host` — the job's steps run directly on the machine, with the tools installed there:
```text
macos:host
```
So a runner registered with
```text
ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest,macos:host
```
runs `runs-on: ubuntu-latest` jobs in the `runner-images:ubuntu-latest` container and `runs-on: macos` jobs directly on the host.
Names may themselves contain a colon, for example `pool:e57e18d4-10d4-406f-93bf-60f127221bdd`; only `host` and `docker` are treated as schemas.
If a job's `runs-on` matches none of the runner's labels, the job still runs, in the default `docker.gitea.com/runner-images:ubuntu-latest` image. Images maintained for this purpose are listed at [gitea/runner-images](https://gitea.com/gitea/runner-images); community images such as the [act images](https://github.com/nektos/act/blob/master/IMAGES.md) work too.
:::note
A runner that only exposes `host` labels still needs access to a Docker daemon whenever a job uses a `docker://` action or a service container. `host` labels only change where the job's own steps run.
:::
## Registration versus config labels
Labels are chosen at registration time (`--labels`, or the interactive prompt) and stored in the registration file. Afterwards they can be changed without re-registering, and the most explicit source wins:
```text
--labels / GITEA_RUNNER_LABELS > runner.labels in the config file > labels in the .runner file
```
- during `register`, `runner.labels` from the config file takes precedence and the `--labels` flag is ignored;
- `daemon --labels` (which defaults to `GITEA_RUNNER_LABELS`) overrides the labels of an already registered runner;
- whenever the resulting labels differ from those in the registration file, they are written back to it and re-declared to the instance on startup;
- labels that fail to parse are skipped with a warning instead of stopping the runner.
Labels can also be edited in the Gitea UI under the runner's settings.
## Choosing images
- Pick an image that already contains the tools your workflows expect. The default images are small; a job that installs a toolchain on every run is usually better served by a purpose-built image.
- Pin images by tag or digest for reproducible jobs. An image pinned by digest is never re-pulled, even with `container.force_pull` enabled.
- Use distinct names such as `linux_amd64:host` or `windows:host` for host labels, so a workflow written for GitHub's `ubuntu-latest` does not accidentally run unsandboxed on your machine.
@@ -0,0 +1,83 @@
---
sidebar_position: 7
---
# Monitoring and health
## Prometheus metrics
```yaml
metrics:
enabled: true
addr: "127.0.0.1:9101"
readiness_grace: 30s
```
With `metrics.enabled`, the runner serves three endpoints on `metrics.addr`:
| Endpoint | Purpose |
| --- | --- |
| `/metrics` | Prometheus exposition of the runner's own metrics |
| `/healthz` | liveness: the process is up |
| `/readyz` | task admission: `503` while a [health check](#local-health-checks) reports the machine as unavailable, once polling has been failing for longer than `readiness_grace`, or after the runner has been deleted on the instance |
There is **no authentication** on this listener. The default address binds to localhost only; expose it more widely (`":9101"`) only behind a firewall or a scrape-only network.
All series are prefixed with `gitea_runner_`, among them:
| Metric | Meaning |
| --- | --- |
| `gitea_runner_info` | always `1`, with the version and runner name as labels |
| `gitea_runner_uptime_seconds` | seconds since the daemon started |
| `gitea_runner_capacity` / `gitea_runner_job_running` / `gitea_runner_job_capacity_utilization_ratio` | configured capacity, jobs in flight, and their ratio |
| `gitea_runner_job_total` | jobs by result (`success`, `failure`, `cancelled`, `skipped`, `unknown`) |
| `gitea_runner_job_duration_seconds` | job duration histogram |
| `gitea_runner_poll_fetch_total` / `gitea_runner_poll_fetch_duration_seconds` | task fetches by result (`task`, `empty`, `error`) and their latency |
| `gitea_runner_poll_backoff_seconds` | last polling backoff interval |
| `gitea_runner_report_log_total` / `gitea_runner_report_state_total` | log and state reports by result |
| `gitea_runner_report_log_buffer_rows` | log rows buffered but not yet sent |
| `gitea_runner_client_errors_total` | RPC errors by method |
Useful things to alert on: `gitea_runner_poll_fetch_total{result="error"}` rising (the instance is unreachable or the runner was deleted), a `capacity_utilization_ratio` pinned at `1` (the pool is too small), and a growing `report_log_buffer_rows` (log delivery is falling behind).
## Local health checks
Health checks let a runner take itself out of rotation when the machine it runs on is not fit for work — most commonly when the disk is full.
```yaml
health_check:
enabled: false
min_free_disk_space_mb: 1024
script: ''
interval: 30s
timeout: 10s
```
- while a check fails, the runner stops fetching **new** tasks; jobs already running are unaffected and finish normally;
- no check runs while a job is active — the last result is reused until the runner is idle again;
- `min_free_disk_space_mb` is measured on the filesystem holding the runner's workspaces, and defaults to 1024 MiB when omitted or zero;
- `script` is any executable. A non-zero exit, a timeout, or a failure to start marks the runner unavailable. Its result is cached for `interval` and it is killed after `timeout`;
- recovery is automatic and logged (`runner local health recovered, resuming task polling`), and the state is reflected by `/readyz`.
## Logs
The runner logs to stderr; `log.level` controls the verbosity, with `debug` and `trace` adding `file:line` to each line. Under systemd the log ends up in the journal (`journalctl -u gitea-runner`), in Docker in `docker logs`.
The runner log is not the job log: step output is streamed to Gitea and is tuned with the `runner.log_report_*` settings, while things like [post-task script](hooks/post-task-script.md) output only ever appear in the runner log.
## Reporting a problem
`gitea-runner bug-report` prints the version, Go version, OS/architecture and CPU count — paste its output into an issue at [gitea/runner](https://gitea.com/gitea/runner/issues):
```bash
gitea-runner bug-report
```
```text
Runner version: 2.3.0
Go version: go1.26.5
OS/Arch: linux/amd64
NumCPU: 8
```
To reproduce a workflow locally, without a Gitea instance and without touching the runner's registration, use `gitea-runner exec` — see the [command line reference](reference/cli.md#exec).
@@ -0,0 +1,185 @@
---
sidebar_position: 1
description: Every gitea-runner command and flag, generated from the runner sources.
---
# Command line reference
{/* Generated by update_runner_docs.sh from the gitea/runner sources, do not edit. */}
`gitea-runner` is a single binary with one subcommand per task. `--config` / `-c` is
global: every command that reads configuration accepts it, and commands that do not
read any ignore it.
## gitea-runner
```text
Gitea Runner
Usage:
gitea-runner [command]
Available Commands:
bug-report Print information useful when filing a bug report
cache-server Start a cache server for the cache action
daemon Run as a runner daemon
exec Run workflow locally.
generate-config Generate an example config file
help Help about any command
register Register a runner to the server
Flags:
-c, --config string Config file path
-h, --help help for gitea-runner
-v, --version version for gitea-runner
Use "gitea-runner [command] --help" for more information about a command.
```
## register
Registers the runner against a Gitea instance and writes the registration file. Interactive unless `--no-interactive` is given; the token can also come from `--token-file` or the `GITEA_RUNNER_REGISTRATION_TOKEN` environment variable. See [Registering a runner](../registration.md).
```text
Register a runner to the server
Usage:
gitea-runner register [flags]
Flags:
--ephemeral Configure the runner to be ephemeral and only ever be able to pick a single job (stricter than --once)
-h, --help help for register
--instance string Gitea instance address
--labels string Runner tags, comma separated
--name string Runner name
--no-interactive Disable interactive mode
--token string Runner token (or set the GITEA_RUNNER_REGISTRATION_TOKEN envvar)
--token-file string Path to a file containing the runner token
Global Flags:
-c, --config string Config file path
```
## daemon
Runs the runner: it polls the instance for jobs and executes them until it is stopped. `--labels` (default: `GITEA_RUNNER_LABELS`) overrides the labels of an already registered runner, and `--once` exits after a single job.
```text
Run as a runner daemon
Usage:
gitea-runner daemon [flags]
Flags:
-h, --help help for daemon
--labels string Runner labels, comma separated. Overrides the labels of an already registered runner
--once Run one job then exit
Global Flags:
-c, --config string Config file path
```
## exec
Runs a workflow from the current repository locally, without a Gitea instance and without the runner configuration file. Useful for debugging a workflow before pushing it. Runner YAML is not loaded, so hooks and cache settings do not apply.
```text
Run workflow locally.
Usage:
gitea-runner exec [flags]
Flags:
--artifact-server-addr string Defines the address where the artifact server listens
--artifact-server-path string Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start. (default ".")
--artifact-server-port string Defines the port where the artifact server listens (will only bind to localhost). (default "34567")
--container-architecture string Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.
--container-cap-add stringArray kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)
--container-cap-drop stringArray kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)
--container-daemon-socket string Path to Docker daemon socket which will be mounted to containers (default "/var/run/docker.sock")
--container-opts string container options
-d, --debug enable debug log
--default-actions-url string Defines the default url of action instance. (default "https://github.com")
--detect-event Use first event type from workflow as event that triggered the workflow
-C, --directory string working directory (default ".")
-n, --dryrun dryrun mode
--env stringArray env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)
--env-file string environment file to read and use as env in the containers (default ".env")
-E, --event string run a event name
-e, --eventpath string path to a JSON event payload file exposed as the event that triggered the workflow
--gitea-instance string Gitea instance to use.
-h, --help help for exec
-i, --image string Docker image to use. Use "-self-hosted" to run directly on the host. (default "docker.gitea.com/runner-images:ubuntu-latest")
--insecure-secrets NOT RECOMMENDED! Doesn't hide secrets while printing logs.
-j, --job string run a specific job ID; when several workflow files define that job, also pass --workflows/-W to select the file
--json Output logs in json format
-l, --list list workflows
--network string Specify the network to which the container will connect
--no-recurse Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag
--no-skip-checkout Do not skip actions/checkout
--privileged use privileged mode
-p, --pull pull docker image(s) even if already present
--rebuild rebuild local action docker image(s) even if already present
-s, --secret stringArray secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)
--use-gitignore Controls whether paths specified in .gitignore should be copied into container (default true)
--userns string user namespace to use
--var stringArray variable to make available to actions with optional value (e.g. --var myvar=foo or --var myvar)
-W, --workflows string path to workflow file(s) (default "./.gitea/workflows/")
Global Flags:
-c, --config string Config file path
```
## cache-server
Runs only the cache server, so several runners can share one cache. `--dir`, `--host` and `--port` override the matching `cache.*` keys; every other setting, `cache.external_secret` included, has to come from the config file. See [Caching](../cache.md#sharing-a-cache-between-runners).
```text
Start a cache server for the cache action
Usage:
gitea-runner cache-server [flags]
Flags:
-d, --dir string Cache directory
-h, --help help for cache-server
-s, --host string Host of the cache server
-p, --port uint16 Port of the cache server
Global Flags:
-c, --config string Config file path
```
## generate-config
Prints the commented example configuration on stdout, which is the starting point for a config file: `gitea-runner generate-config > config.yaml`.
```text
Generate an example config file
Usage:
gitea-runner generate-config [flags]
Flags:
-h, --help help for generate-config
Global Flags:
-c, --config string Config file path
```
## bug-report
Prints the runner version, Go version, OS/architecture and CPU count, for pasting into an issue.
```text
Print information useful when filing a bug report
Usage:
gitea-runner bug-report [flags]
Flags:
-h, --help help for bug-report
Global Flags:
-c, --config string Config file path
```
@@ -0,0 +1,248 @@
---
sidebar_position: 2
description: The commented example configuration of the runner, generated from the runner sources.
---
# Example configuration
{/* Generated by update_runner_docs.sh from the gitea/runner sources, do not edit. */}
This is the output of `gitea-runner generate-config`. It is safe to use unmodified,
and it is the authoritative list of every option the runner understands. See
[Configuration](../configuration.md) for what the options mean and how the file is
loaded.
```yaml
# Example configuration file, it's safe to copy this as the default config file without any modification.
# You don't have to copy this file to your instance,
# just run `./gitea-runner generate-config > config.yaml` to generate a config file.
# Logging for the runner process itself (messages printed to stderr).
# This does not control how workflow step output is streamed to the Gitea UI;
# tune that with runner.log_report_* below.
log:
# logrus severity: trace, debug, info, warn, error, fatal, panic.
# trace and debug turn on caller/file:line in log lines. Default if omitted: info.
level: info
runner:
# Where to store the registration result.
file: .runner
# Execute how many tasks concurrently at the same time.
capacity: 1
# Extra environment variables to run jobs.
envs:
A_TEST_ENV_NAME_1: a_test_env_value_1
A_TEST_ENV_NAME_2: a_test_env_value_2
# Extra environment variables to run jobs from a file.
# It will be ignored if it's empty or the file doesn't exist.
env_file: .env
# The timeout for a job to be finished.
# Please note that the Gitea instance also has a timeout (3h by default) for the job.
# So the job could be stopped by the Gitea instance if its timeout is shorter than this.
timeout: 3h
# The timeout for the runner to wait for running jobs to finish when shutting down.
# Any running jobs that haven't finished after this timeout will be cancelled.
shutdown_timeout: 0s
# Whether skip verifying the TLS certificate of the Gitea instance.
insecure: false
# The timeout for fetching the job from the Gitea instance.
fetch_timeout: 5s
# The interval for fetching the job from the Gitea instance.
fetch_interval: 2s
# The maximum interval for fetching the job from the Gitea instance.
# The runner uses exponential backoff when idle, increasing the interval up to this maximum.
# Set to 0 or same as fetch_interval to disable backoff.
fetch_interval_max: 5s
# While idle, remove stale bind-workdir task directories and orphaned host-mode
# scratch directories (left behind when a host cleanup delete stalls) older than
# this duration. Setting either workdir_cleanup_age or idle_cleanup_interval to 0
# (or any non-positive value) disables stale-directory cleanup entirely.
workdir_cleanup_age: 24h
# Cadence for the idle stale-directory cleanup pass.
idle_cleanup_interval: 10m
# The base interval for periodic log flush to the Gitea instance.
# Logs may be sent earlier if the buffer reaches log_report_batch_size
# or if log_report_max_latency expires after the first buffered row.
log_report_interval: 5s
# The maximum time a log row can wait before being sent.
# This ensures even a single log line appears on the frontend within this duration.
# Must be less than log_report_interval to have any effect.
log_report_max_latency: 3s
# Flush logs immediately when the buffer reaches this many rows.
# This ensures bursty output (e.g., npm install) is delivered promptly.
log_report_batch_size: 100
# The interval for reporting task state (step status, timing) to the Gitea instance.
# State is also reported immediately on step transitions (start/stop).
state_report_interval: 5s
# Per-attempt deadline for flushing the final logs and task state when a job
# finishes, on a detached context so a server cancel can't block the acknowledgement.
report_close_timeout: 10s
# The github_mirror of a runner is used to specify the mirror address of the github that pulls the action repository.
# It works when something like `uses: actions/checkout@v4` is used and DEFAULT_ACTIONS_URL is set to github,
# and github_mirror is not empty. In this case,
# it replaces https://github.com with the value here, which is useful for some special network environments.
github_mirror: ''
# When true (the default), fetch only the requested ref of an action repository (e.g. actions/checkout@v4) at depth 1 instead of cloning every branch's full history.
# Set to false to clone the full history.
action_shallow_clone: true
# When true (the default), inject the ACT=true environment variable into jobs.
# Set to false so workflows gated on `if: ${{ !env.ACT }}` behave like they do on GitHub.
set_act_env: true
# The labels of a runner are used to determine which jobs the runner can run, and how to run them.
# Like: "macos-arm64:host" or "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
# Find more images provided by Gitea at https://gitea.com/gitea/runner-images .
# If it's empty when registering, it will ask for inputting labels.
# If it's empty when execute `daemon`, will use labels in `.runner` file.
labels:
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
- "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
# Allocate a pseudo-TTY for each step's process. Applies to both host and docker backends.
# Default false matches GitHub actions/runner. Enable only for jobs that need an interactive
# terminal; tools like `docker build` emit redrawing progress frames into the captured log
# when a TTY is present.
allocate_pty: false
# Optional executable on the host, run once after each task's built-in cleanup
# (post-steps, container teardown, bind-workdir removal). Additive only.
#
# IMPORTANT: While this script runs the runner stops task heartbeats and stays
# offline from Gitea's perspective until the script exits. A script that never
# returns blocks new work until post_task_script_timeout kills it (default 5m).
# Keep scripts short; set post_task_script_timeout to a safe upper bound.
#
# Output -> runner process log (not the job log). Non-zero exit -> warning only.
# Windows: use .exe, .bat, or .cmd. PowerShell (.ps1) is not supported yet as
# the configured path; wrap PowerShell commands in a .cmd file instead.
# Full guide: docs/post-task-script.md
post_task_script: ''
# Hard limit on post_task_script runtime. Default if omitted: 5m.
post_task_script_timeout: 5m
cache:
# Enable the built-in cache server (used by actions/cache and similar actions).
enabled: true
# Directory where cache blobs are stored on disk. Default: $HOME/.cache/actcache
# Ignored when external_server is set.
dir: ""
# Outbound IP or hostname that job containers use to reach this runner's cache server.
# Leave empty to detect automatically. 0.0.0.0 is not valid here.
# If the runner itself runs in Docker, automatic detection can choose an
# address on the runner container's network that job containers cannot reach
# when the runner creates a separate per-job network. In that case, set this
# to a hostname/IP reachable from job containers, and set port to a fixed
# published port or put the job containers on a shared Docker network.
# Ignored when external_server is set.
host: ""
# Port for the built-in cache server. 0 picks a random free port.
# Ignored when external_server is set.
port: 0
# URL of a shared `gitea-runner cache-server` to use instead of starting a local one.
# Set on every runner that should share a cache pool. Must end with "/".
# Example: "http://cache-host:8088/"
# Requires external_secret (below) to match the value on the cache-server.
external_server: ""
# Shared secret between this runner and the external cache-server.
# Required when external_server is set. Must be identical on every runner and the cache-server.
# Generate with: openssl rand -hex 32
external_secret: ""
# Path to a file containing the shared secret, as an alternative to external_secret.
# Use this to keep the secret out of this file.
# Surrounding whitespace is trimmed, so a trailing newline in the file is fine.
# Setting both external_secret and external_secret_file is an error.
external_secret_file: ""
# When true, reuse a cached action instead of fetching from the remote on every job.
# A moved tag (e.g. a re-tagged "v6") or an updated branch stays at the cached commit
# until its cache entry expires or is manually removed.
offline_mode: false
container:
# Specifies the network to which the container will connect.
# Could be host, bridge or the name of a custom network.
# If it's empty, runner will create a network automatically.
# For dockerized runners using the built-in cache server, a custom shared
# network can be required so job containers can reach cache.host/cache.port.
# Deprecated: `network_mode` is still accepted for old configs; use `network` instead.
network: ""
# network_create_options only apply when `network` is left empty and the runner
# auto-creates a per-job network that does not already exist. They have no effect
# when a custom `network` name is set, because that network is used as-is and never
# created by the runner. Omit the entire block to use Docker's defaults.
network_create_options:
enable_ipv4: true # Omit to use Docker's default (IPv4 enabled). Set false to disable IPv4.
enable_ipv6: false # Omit to use Docker's default (IPv6 disabled). Enabling it requires dockerd started with --ipv6.
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
privileged: false
# Any other options to be used when the container is started (e.g., --add-host=my.gitea.url:host-gateway).
options:
# The parent directory of a job's working directory.
# NOTE: There is no need to add the first '/' of the path as runner will add it automatically.
# If the path starts with '/', the '/' will be trimmed.
# For example, if the parent directory is /path/to/my/dir, workdir_parent should be path/to/my/dir
# If it's empty, /workspace will be used.
# Purely numeric subdirectories under this path are reserved for task workspaces and may be removed by idle cleanup.
workdir_parent:
# Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob
# You can specify multiple volumes. If the sequence is empty, no volumes can be mounted.
# For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to:
# valid_volumes:
# - data
# - /src/*.json
# If you want to allow any volume, please use the following configuration:
# valid_volumes:
# - '**'
valid_volumes: []
# Overrides the docker client host with the specified one.
# If it's empty, runner will find an available docker host automatically.
# If it's "-", runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers.
# If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
docker_host: ""
# Pull docker image(s) even if already present.
# Defaults to false when the key is omitted.
force_pull: false
# Rebuild docker image(s) even if already present
force_rebuild: false
# Always require a reachable docker daemon, even if not required by runner
require_docker: false
# Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or runner
docker_timeout: 0s
# Bind the workspace to the host filesystem instead of using Docker volumes.
# This is required for Docker-in-Docker (DinD) setups when jobs use docker compose
# with bind mounts (e.g., ".:/app"), as volume-based workspaces are not accessible
# from the DinD daemon's filesystem. When enabled, ensure the workspace parent
# directory is also mounted into the runner container and listed in valid_volumes.
bind_workdir: false
host:
# The parent directory of a job's working directory.
# If it's empty, $HOME/.cache/act/ will be used.
workdir_parent:
# Optional local task-admission checks. Disabled by default. When enabled, low
# disk space or a failing script pauses new task fetching; existing jobs continue.
# No health checks run while any job is active; the last result is reused until idle.
health_check:
enabled: false
# Minimum free space required on the filesystem holding runner workspaces.
# Defaults to 1024 MiB when omitted or set to zero.
min_free_disk_space_mb: 1024
# Optional additional executable. A non-zero exit, timeout, or startup failure
# marks the runner unavailable.
script: ''
# How long a script result is cached and its maximum execution time.
interval: 30s
timeout: 10s
metrics:
# Enable the Prometheus metrics endpoint.
# When enabled, metrics are served at /metrics, liveness at /healthz, and
# task-admission readiness at /readyz.
enabled: false
# The address for the metrics HTTP server to listen on.
# Defaults to localhost only. Set to ":9101" to allow external access,
# but ensure the port is firewall-protected as there is no authentication.
addr: "127.0.0.1:9101"
# Consecutive polling failures may last this long before /readyz returns 503.
readiness_grace: 30s
```
@@ -0,0 +1,85 @@
---
sidebar_position: 2
---
# Registering a runner
A runner has to be registered before it can pick up jobs: registration is what tells the runner where to fetch jobs from, and what gives the Gitea instance a stable identity for the runner.
## Obtain a registration token
Registration tokens are issued by the Gitea instance and can be scoped to the whole instance, an organization/user, or a single repository. See [Actions runners](/usage/actions/runner) for where to find them in the UI and via the API.
One token can register any number of runners and stays valid until it is reset in the UI or through the API. Instance administrators can also hand Gitea a fixed token with `GITEA_RUNNER_REGISTRATION_TOKEN` / `GITEA_RUNNER_REGISTRATION_TOKEN_FILE` at startup, which is what makes disposable runners practical.
## Interactive registration
```bash
gitea-runner register
# or with a config file
gitea-runner -c config.yaml register
```
The runner asks for:
- the instance URL, e.g. `https://gitea.com/` or `http://192.168.8.8:3000/` — use the instance's `ROOT_URL`, not `localhost`, when Gitea and the runner are in different containers or hosts;
- the registration token;
- the runner name, defaulting to the hostname;
- the [labels](labels.md), defaulting to the built-in list, or to `runner.labels` when the config file sets it.
## Non-interactive registration
```bash
gitea-runner register --no-interactive \
--instance <instance_url> \
--token <registration_token> \
--name <runner_name> \
--labels <runner_labels>
```
The token can be kept off the command line, where it would show up in the process list and in shell history:
- `--token-file <path>` reads it from a file, e.g. a Docker or Kubernetes secret;
- the `GITEA_RUNNER_REGISTRATION_TOKEN` environment variable is used when neither flag is given.
## The registration file
A successful registration writes a `.runner` file (`runner.file` in the config) into the current working directory. It holds the runner's identity and its API credentials, so:
- do not edit it by hand, and do not copy it to a second machine;
- back it up or put it on a persistent volume, otherwise a recreated runner registers as a *new* runner and leaves a stale entry behind — and fails outright if the token has been reset in the meantime;
- if it is lost or corrupted, delete it and register again.
Each runner process needs its own registration file. Two processes sharing one file are indistinguishable to Gitea and cancel each other's jobs; this version does not detect that, so give every runner its own `runner.file` or its own working directory. Runner `3.0` refuses to start in that situation.
## Ephemeral runners
An ephemeral runner accepts exactly one job and then exits. Once a job has been assigned, its credentials are revoked, so it cannot poll for more work before the job's untrusted code runs; it can still report progress until the job finishes.
This is how organization-wide or instance-wide runners can be offered without trusting every repository that may use them, provided each runner is a fresh VM or container.
```bash
gitea-runner register --ephemeral
gitea-runner register --no-interactive --ephemeral --instance <instance_url> --token <registration_token>
```
With the Docker images, set `GITEA_RUNNER_EPHEMERAL=1` instead; no `/data` volume is needed, since the credentials are single-use:
```bash
docker run -d --name my_runner \
-e GITEA_INSTANCE_URL=<instance_url> \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<registration_token> \
-e GITEA_RUNNER_EPHEMERAL=1 \
-v /var/run/docker.sock:/var/run/docker.sock \
docker.io/gitea/runner:2
```
Because a fresh registration is required for every job, ephemeral runners are usually started on demand from the `workflow_job` webhook, which fires when a job is queued.
`--ephemeral` is stricter than `daemon --once`: `--once` also stops after one job, but its credentials stay valid for as long as the runner is registered.
## Re-registering and unregistering
Running `register` again in a directory that already has a registration file asks whether to overwrite it. To retire a runner, delete it in the Gitea UI (or via the API) and remove its registration file; the daemon shuts itself down once the server no longer knows it.
Changing labels does not require re-registration — see [Labels](labels.md#registration-versus-config-labels).
@@ -0,0 +1,42 @@
---
sidebar_position: 8
---
# Upgrading
A runner upgrade is a binary or image replacement: stop the daemon, swap it, start it again. The registration file stays valid across versions, so a runner keeps its identity and does not have to be registered again.
```bash
sudo systemctl stop gitea-runner
sudo install -m 0755 gitea-runner-<version>-linux-amd64 /usr/local/bin/gitea-runner
sudo systemctl start gitea-runner
```
With `runner.shutdown_timeout` set, `stop` lets the jobs in flight finish first; without it, they are cancelled and Gitea reschedules them.
Config files are read leniently: keys the running version does not know are reported as a warning and ignored, so one file can be shared by runners of different versions. Compare your file with `gitea-runner generate-config` after an upgrade to pick up new options.
## 2.0
### Breaking
- **`DOCKER_USERNAME` / `DOCKER_PASSWORD` are no longer implicit pull credentials.** They used to be attached to every image pull, which sent private-registry credentials to Docker Hub for public images. They are ordinary secrets now. Migrate to:
- `container.credentials` (and service `credentials`) in the workflow for private images;
- a `docker login` performed on the runner host, for private `uses: docker://...` actions.
- **`container.force_pull` now defaults to `false`** in the generated example config, so images already present are reused unless you ask for a pull.
- **No environment variable overrides for the config.** `GITEA_DEBUG`, `GITEA_TRACE`, `GITEA_RUNNER_CAPACITY`, `GITEA_RUNNER_FILE`, `GITEA_RUNNER_ENVIRON` and `GITEA_RUNNER_ENV_FILE` are ignored; use a config file. The Docker images' [entrypoint variables](installation/docker.md#entrypoint-environment-variables) are unaffected.
### Also new
- [Post-task script](hooks/post-task-script.md) (`runner.post_task_script`) for host housekeeping between jobs.
- [Health checks](monitoring.md#local-health-checks) (`health_check.*`) that pause task admission on low disk space or a failing script.
- `register --token-file`, and `GITEA_RUNNER_LABELS` honoured by `daemon`, so labels can change without re-registering.
- `jobs.<job_id>.timeout-minutes` and `jobs.<job_id>.continue-on-error` support, job summaries, shallow action clones (`runner.action_shallow_clone`), `ssh://` action URLs, IPv4/IPv6 options for auto-created networks, `--platform` and `--pull` in `container.options`, `cache.external_secret_file`, a GitHub-style "Set up job" log section, and pre/post entrypoints of Docker actions.
## Moving to 3.0
`3.0` filters host-escaping options out of a workflow's `container.options` while privileged mode is off, refuses to start a second process on the same registration file, and serves cache service v2 by default. See the [3.0 documentation](/runner/upgrade) before upgrading.
## Downgrading
Downgrading a runner is possible — the registration file format has not changed — but a config file written for a newer version may carry keys the older one ignores, and features such as cache service v2 stop being served, so jobs that came to rely on them fail. Test a downgrade with a spare runner before doing it on a busy pool.