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,109 @@
name: update runner reference
on:
schedule:
- cron: '30 3 * * 1' # every Monday at 03:30
workflow_dispatch:
env:
# main is protected, so the update is proposed as a pull request from this branch
BOT_BRANCH: bot/update-runner-reference
BASE_BRANCH: main
# the develop runner docs follow the main branch of gitea/runner, every
# documented release series follows its newest stable tag. The value is used
# unquoted, so the glob is expanded by the shell of each step.
TARGET_DIRS: runner-docs/reference runner-docs_versioned_docs/version-*/reference
jobs:
update-runner-reference:
if: github.repository == 'gitea/docs'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: main
# pushing uses DEPLOY_TOKEN, keep the ephemeral job token out of .git/config
persist-credentials: false
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version: 'stable'
cache: false
- name: regenerate the runner reference pages
run: |
set -euo pipefail
./update_runner_docs.sh main runner-docs/reference
./update_runner_docs.sh --released
- name: verify the generated files
run: |
set -euo pipefail
# the pages are built from `--help` and `generate-config`, so a truncated
# build would silently produce an almost empty page
for dir in $TARGET_DIRS; do
echo "checking $dir"
grep -q 'gitea-runner \[command\]' "$dir/cli.md"
grep -q 'gitea-runner exec \[flags\]' "$dir/cli.md"
grep -q '^runner:' "$dir/config-example.md"
done
- name: push bot branch
id: bot_branch
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
set -euo pipefail
if git diff --quiet -- $TARGET_DIRS; then
echo "the runner reference is already up to date"
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -z "$DEPLOY_TOKEN" ]; then
echo "secrets.DEPLOY_TOKEN is missing, cannot push the update"
exit 1
fi
remote="https://x-access-token:$DEPLOY_TOKEN@${GITHUB_SERVER_URL#*://}/$GITHUB_REPOSITORY.git"
# skip if an open bot branch already carries exactly these files
if git fetch --quiet --depth=1 "$remote" "refs/heads/$BOT_BRANCH" 2>/dev/null; then
if git diff --quiet FETCH_HEAD -- $TARGET_DIRS; then
echo "$BOT_BRANCH already proposes this runner reference"
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
fi
git config user.name "Gitea Bot"
git config user.email "[email protected]"
git switch --create "$BOT_BRANCH"
git add $TARGET_DIRS
git commit -m "Update the runner reference pages"
# force push: the branch is always rebuilt on top of the current main
git push --force "$remote" "HEAD:refs/heads/$BOT_BRANCH"
echo "changed=true" >> "$GITHUB_OUTPUT"
- name: create pull request
if: steps.bot_branch.outputs.changed == 'true'
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
set -euo pipefail
cat > pull.json <<EOF
{
"base": "$BASE_BRANCH",
"head": "$BOT_BRANCH",
"title": "Update the runner reference pages",
"body": "Automated update of the generated runner reference pages from gitea/runner (main for the develop docs, the newest stable tag for every documented series), opened by the \"update runner reference\" scheduled workflow."
}
EOF
code="$(curl --silent --show-error --output pull-response.json --write-out '%{http_code}' \
-X POST "${GITHUB_API_URL:-$GITHUB_SERVER_URL/api/v1}/repos/$GITHUB_REPOSITORY/pulls" \
-H "Authorization: token $DEPLOY_TOKEN" \
-H 'Content-Type: application/json' \
--data @pull.json)"
case "$code" in
201) echo "pull request created" ;;
409) echo "an open pull request for $BOT_BRANCH already exists, it now points at the new commit" ;;
*) echo "unexpected response $code:"; cat pull-response.json; exit 1 ;;
esac
+12
View File
@@ -55,3 +55,15 @@ update-api-docs:
.PHONY: update-api-docs-latest .PHONY: update-api-docs-latest
update-api-docs-latest: update-api-docs-latest:
./update_api_docs.sh --latest-only ./update_api_docs.sh --latest-only
# regenerate the generated runner reference pages of the develop docs from the
# main branch of gitea/runner (used by the update runner reference cron job)
.PHONY: update-runner-docs
update-runner-docs:
./update_runner_docs.sh main runner-docs/reference
# same, for every documented release series: the tags are looked up through the
# Gitea API, so a runner release needs no change here
.PHONY: update-runner-docs-released
update-runner-docs-released:
./update_runner_docs.sh --released
+44
View File
@@ -35,3 +35,47 @@ make update-api-docs-latest # refresh only static/swagger-latest.json
`static/swagger-latest.json` is refreshed automatically: the `update swagger files` `static/swagger-latest.json` is refreshed automatically: the `update swagger files`
workflow runs every 12 hours and opens a pull request whenever gitea main changed. workflow runs every 12 hours and opens a pull request whenever gitea main changed.
Released versions are updated by hand when a new gitea version is documented. Released versions are updated by hand when a new gitea version is documented.
## Runner docs
The runner documentation is a second docs plugin, served under `/runner`:
| Version | Content | URL |
| --- | --- | --- |
| develop | `runner-docs/` | `/runner/develop/` |
| current series | `runner-docs_versioned_docs/version-3/` | `/runner/` |
| older series | `runner-docs_versioned_docs/version-2/` | `/runner/2/` |
| archived series | `runner-docs_versioned_docs/version-1/` | `/runner/1/` |
A version directory covers a whole release series (`version-3` documents every
`3.x` release), so a patch release only needs a content update, not a new folder.
The UI labels a series `3.x`, derived from `runner-docs_versions.json`, so no
version number has to be bumped anywhere on a runner release. Use floating image
tags (`gitea/runner:3`) and links to the runner's `main` branch in those pages to
keep them valid across patch releases.
Its sidebar is written by hand: `runner-sidebars.js` for develop, and
`runner-docs_versioned_sidebars/version-<version>-sidebars.json` per documented
version. The version list lives in `runner-docs_versions.json`; the `versions`,
`lastVersion` and `Runner Version` dropdown entries in `docusaurus.config.js` are
built from it, so a new series only has to be cut with
`pnpm run docusaurus docs:version:runner-docs <series>`.
The pages under `reference/` are generated from the runner sources — the command
line reference from `--help`, the example configuration from `generate-config`:
```shell
make update-runner-docs # develop docs, from the main branch of gitea/runner
make update-runner-docs-released # every series, from its newest stable tag
./update_runner_docs.sh v3.0.2 runner-docs_versioned_docs/version-3/reference
```
`--released` (what `make update-runner-docs-released` runs) needs no version list:
it regenerates every `runner-docs_versioned_docs/version-<series>/reference`
directory from the newest stable `v<series>.x.y` tag of `gitea/runner`, looked up
through the Gitea API.
All of these pages are refreshed automatically: the `update runner reference`
workflow runs weekly, regenerates the develop and the released references, and
opens a pull request whenever the runner's CLI or example configuration changed.
Generating them needs Go, since the script builds the runner binary.
+30 -25
View File
@@ -169,6 +169,16 @@ const versions = {
}, },
}; };
// The runner docs keep one directory per release series
// (runner-docs_versioned_docs/version-<series>), so a series is labelled "3.x"
// and a patch release never touches this file. The list is the one docusaurus
// maintains, newest first.
const runnerVersions = require("./runner-docs_versions.json");
const runnerVersionLabel = (version) => `${version}.x`;
const runnerVersionPath = (version) =>
// no path for the latest series, its docs are served at /runner/
version === runnerVersions[0] ? "/runner/" : `/runner/${version}/`;
/** @type {import('@docusaurus/types').Config} */ /** @type {import('@docusaurus/types').Config} */
const config = { const config = {
title: "Gitea Documentation", title: "Gitea Documentation",
@@ -196,23 +206,25 @@ const config = {
id: "runner-docs", id: "runner-docs",
path: "runner-docs", path: "runner-docs",
routeBasePath: "runner", routeBasePath: "runner",
//sidebarPath: './runner/sidebars.js', sidebarPath: require.resolve("./runner-sidebars.js"),
// the "current" runner docs are only a stub that re-uses the main // the current runner docs describe the main branch of gitea/runner
// docs runner page and do not describe the development version includeCurrentVersion: true,
// correctly, so they are not published
includeCurrentVersion: false,
versions: { versions: {
"1.0.8": { current: {
label: "1.0.8", path: "develop",
}, label: "develop",
"0.2.11": { banner: "unreleased",
path: "0.2.11",
label: "0.2.11",
}, },
...Object.fromEntries(
runnerVersions.map((version) => [
version,
{ label: runnerVersionLabel(version) },
]),
),
}, },
// no "path" for lastVersion, so the latest stable runner docs are // the newest series has no "path", so the latest stable runner docs are
// served at /runner/ and links do not need updating on each release // served at /runner/ and links do not need updating on each release
lastVersion: "1.0.8", lastVersion: runnerVersions[0],
editUrl: ({ editUrl: ({
versionDocsDirPath, versionDocsDirPath,
docPath, docPath,
@@ -226,16 +238,6 @@ const config = {
: `runner-docs_versioned_docs/version-${version}` : `runner-docs_versioned_docs/version-${version}`
}/${docPath}`; }/${docPath}`;
}, },
async sidebarItemsGenerator({ defaultSidebarItemsGenerator, ...args }) {
const { item } = args;
// Use the provided data to generate a custom sidebar slice
const sidebarItems = await defaultSidebarItemsGenerator(args);
if (item.dirName !== "usage") {
return sidebarItems;
} else {
return sortItemsByCategory(sidebarItems);
}
},
}, },
], ],
], ],
@@ -450,8 +452,11 @@ const config = {
label: "Runner Version", label: "Runner Version",
position: "right", position: "right",
items: [ items: [
{ to: "/runner/", label: "1.0.8" }, { to: "/runner/develop/", label: "develop" },
{ to: "/runner/0.2.11/", label: "0.2.11" }, ...runnerVersions.map((version) => ({
to: runnerVersionPath(version),
label: runnerVersionLabel(version),
})),
], ],
routerRgx: "/runner/", routerRgx: "/runner/",
classNames: "runner-dropdown", classNames: "runner-dropdown",
+107
View File
@@ -0,0 +1,107 @@
---
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.
## Cache service v2
`actions/[email protected]` and later can use the *cache service v2* API. The runner serves it from the same store as v1, on by default, and it also works behind a shared cache server. Turn it off with:
```yaml
cache:
v2: false
```
Those actions refuse any host they do not take for GitHub. Rather than misreport the server URL, the runner edits that check out of the action's own JavaScript bundle and keeps the untouched copy beside it; a bundle it does not recognise is left alone and keeps to v1. The same edit lets the stock `actions/upload-artifact` and `actions/download-artifact` work from `v4.4.0` on, without the `gitea-upload-artifact` fork.
Because `ACTIONS_RESULTS_URL` names one origin that has to serve the whole results API, the cache server also forwards the artifact half of it to the Gitea instance the job belongs to. Jobs are therefore pointed at the cache server, which is what makes clients such as `docker buildx` find the cache service instead of receiving a 404 from Gitea.
## 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:nightly
```
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.
+138
View File
@@ -0,0 +1,138 @@
---
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. |
| `hooks.job_started` / `hooks.job_completed` | | scripts run inside the job environment, see [Job hooks](hooks/job-hooks.md). |
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 left behind by jobs the runner did not live to tear down are removed. They are recognised by the `com.gitea.runner.uuid` label carrying this runner's uuid, so leftovers of other runners on the same daemon are left alone. Without this, each leaked network keeps holding a subnet of the daemon's address pool.
## `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. |
| `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. |
| `v2` | `true` | serve the cache service v2 API used by `actions/[email protected]` and later. |
## `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`. A volume declared here replaces the one the runner mounts on the same container path, which is how the tool cache can be kept on the host (`--volume /host/toolcache:/opt/hostedtoolcache`); its source must also be allowed by `valid_volumes`. |
| `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. Images pinned by digest are never re-pulled, and a failed pull with a local copy available only warns. |
| `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`. |
:::note Privileged mode and workflow container options
A workflow's own `jobs.<job_id>.container.options` are untrusted input. While `container.privileged` is disabled, the options that would break out of the container are stripped with a warning in the job log: `--pid`, `--ipc`, `--uts`, `--cgroupns`, `--userns`, `--cap-add`, `--security-opt`, `--device`, `--device-cgroup-rule`, `--gpus`, `--volumes-from`, `--runtime`, `--cgroup-parent` and `--sysctl`. They are honoured once privileged mode is enabled, because the operator has then opted into host access.
:::
## `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.
+74
View File
@@ -0,0 +1,74 @@
---
sidebar_position: 1
---
# Job hooks
Job hooks are operator-provided scripts that run **inside the job environment**, before the job's first step and after its last one. They are the equivalent of GitHub's [job hooks](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/run-scripts):
```yaml
runner:
hooks:
job_started: /hooks/started.sh
job_completed: /hooks/completed.sh
```
| Setting | Runs |
| --- | --- |
| `runner.hooks.job_started` | before the job's first step, before any action is downloaded |
| `runner.hooks.job_completed` | after the job's last post step, while the job environment is still up |
`ACTIONS_RUNNER_HOOK_JOB_STARTED` and `ACTIONS_RUNNER_HOOK_JOB_COMPLETED` are read from the runner's environment (`runner.envs`, `runner.env_file`) when the settings are unset, so a configuration carried over from `actions/runner` keeps working. The settings take precedence. A workflow cannot point the runner at a different hook: those variables are only read from the runner's own environment, never from the job's.
Both hooks are **synchronous** and block the job while they run, and a non-zero exit from either one fails the job. There is no `continue-on-error` and no per-hook timeout — the job's `runner.timeout` is the only bound. Run anything long in the background from within the hook.
Use them for per-job setup that no workflow should have to carry: registry logins, mirror configuration, or masking runner-wide secrets with `::add-mask::`.
## Where they run
The hooks run where the job's steps run: inside the job container, or on the host in host mode. The paths are resolved *there*, so the script has to exist in the job image or on the host — a path that only exists on the runner host is not visible to a containerized job. For host-wide cleanup after the job environment is gone, use the [post-task script](post-task-script.md) instead.
:::note
This is a deliberate difference from `actions/runner`, which runs its job hooks on the host, outside any container the job declares. Running them where the steps run is what lets a hook prepare the environment the steps actually see.
:::
The script is run according to its extension:
| Extension | Command |
| --- | --- |
| `.sh` | `bash -e <path>` |
| `.ps1` | `pwsh -command . '<path>'` |
| anything else | the file itself, which needs its own shebang and executable bit |
As on GitHub, the shell flags applied to `run:` steps are **not** applied to a hook — set `pipefail` or anything else you want inside the script.
A hook path that does not exist inside the job environment fails the job with `No such file or directory`, naming the path.
### Docker-in-Docker and Docker-out-of-Docker
The hook is executed and its files are exchanged over the Docker API, addressed by container ID, so no path is translated between the runner and the daemon. Both setups work unchanged, but they differ in where the hook file has to be:
- **DinD** — the daemon has its own filesystem. Bake the hook into the job image; a path from the runner's filesystem is not visible to it.
- **DooD** — the job container is created by the host's daemon, so a bind mount in `container.options` is resolved against the **host**, not against the runner container. Either bake the hook into the job image, or mount a host directory and add it to `container.valid_volumes`.
## Environment
A hook sees the job's environment: the workflow, job and `container:` `env:`, the runner's `envs`, and the `GITHUB_*` context variables, with the same masking applied to its output as to a step's. The step-specific ones (`GITHUB_ACTION`, `GITHUB_OUTPUT`, `GITHUB_STATE`) are not set — a hook is not a step, so `::save-state::` and `::set-output::` have nowhere to go.
Its stdout is part of the job log, inside a collapsible group, and is scanned for workflow commands: `::add-mask::` registers a value to be masked for the rest of the job, `::set-env::` and `::add-path::` apply to the steps that follow.
`$GITHUB_ENV` and `$GITHUB_PATH` point at files that are read back after the hook exits, so the file-command form works too:
```bash
#!/bin/bash
echo "REGISTRY_TOKEN=$(fetch-token)" >> "$GITHUB_ENV"
echo "/opt/tooling/bin" >> "$GITHUB_PATH"
```
Both files are the hook's own, separate from the per-step ones, so nothing a hook writes is truncated by the first step.
## Recommendations
- Keep hooks **fast** and return the right exit code: they are on the critical path of every job, and nothing bounds them.
- Use **idempotent** operations, and expect `job_completed` to run after success, failure, and cancellation alike.
- Mask anything secret the hook prints or exports with `::add-mask::`.
+101
View File
@@ -0,0 +1,101 @@
---
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.
- For work that has to happen inside the job environment, use [job hooks](job-hooks.md) instead.
+50
View File
@@ -0,0 +1,50 @@
---
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 Development version
These pages describe the `main` branch of `gitea.com/gitea/runner`, which is published as the `nightly` binaries and images.
Features documented here may not be part of a release yet. Pick a released version in the **Runner Version** dropdown for stable documentation.
:::
## 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), [job hooks](hooks/job-hooks.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` and `3.0.0` both contain breaking changes.
-10
View File
@@ -1,10 +0,0 @@
---
sidebar_position: 1
slug: /
---
# Gitea Runner
The [Gitea Runner](https://gitea.com/gitea/runner) is the runner for Gitea Actions. This section contains versioned documentation for the runner.
See the latest stable documentation at [Gitea Runner](/runner/).
+153
View File
@@ -0,0 +1,153 @@
---
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=nightly
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).
Environment variables for the process — most importantly [proxy variables](../proxy.md) — belong in `Environment=` lines or a drop-in file, not in the runner config.
## 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.
+147
View File
@@ -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`.
`latest` is the newest release, `nightly` is built from the `main` branch, and every release is also tagged with its version.
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 |
| --- | --- | --- | --- | --- |
| `nightly`, `latest`, `<version>` | `alpine` | none, you provide one | `tini` | `root` |
| `nightly-dind`, `latest-dind` | `docker:dind` | bundled, needs `--privileged` | `s6` | `root` |
| `nightly-dind-rootless`, `latest-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:nightly
```
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:nightly-dind
```
`s6` starts `dockerd` first and the runner service waits for it before registering. Use `nightly-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:nightly gitea-runner generate-config > config.yaml
```
## docker compose
```yaml
services:
runner:
image: docker.io/gitea/runner:nightly
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:nightly-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.
+49
View File
@@ -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.
+70
View File
@@ -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.
+83
View File
@@ -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: nightly
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).
+46
View File
@@ -0,0 +1,46 @@
---
sidebar_position: 6
---
# Proxies
The runner reads the usual proxy variables from its own environment and passes them on to every job:
```sh
http_proxy=http://proxy.example:3128
https_proxy=http://proxy.example:3128
no_proxy=gitea.internal,.example.local
```
Set them where the process is started — `Environment=` in a systemd unit, `docker run -e`, or `env:` in Kubernetes. They are used for the runner's own requests and are given to jobs in both lower and upper case.
## What is exempted automatically
These are added to `no_proxy` for jobs, so they are always reached directly:
- the cache server
- `localhost`, `127.0.0.1` and `::1`
- the job's service containers
- the Docker daemon, when it is reached over `tcp://`
The Gitea instance is **not** added. Add it to `no_proxy` yourself if it should be reached directly.
## Overriding per job or per runner
| Scope | Where to set it |
| --- | --- |
| one step | the step's `env:` |
| one job | the job's `container.env` |
| the whole runner | `runner.envs` in the config; a `no_proxy` set there is added to the list above instead of replacing it |
Setting proxy variables at workflow or job level (outside `container.env`) has no effect.
## The Docker daemon needs its own setting
Images are pulled by the daemon, not by the runner, so the daemon needs its own proxy configuration. In the `dind` images the daemon shares the container's environment and picks the variables up; for any other daemon see [the Docker documentation](https://docs.docker.com/engine/daemon/proxy/). The runner logs a warning at startup when it has a proxy configured and the daemon does not.
Dockerfile actions are built with these variables passed as build arguments, so their `RUN` steps can reach the network.
## Credentials in proxy URLs
A password inside a proxy URL is masked in job logs, but any step can still read it: the step is given the proxy URL in its environment. Prefer a proxy that does not need credentials, or one that authenticates by source address.
+185
View File
@@ -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
```
+276
View File
@@ -0,0 +1,276 @@
---
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.
# With `container.network` empty, each 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 docker daemon config.
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, along with
# the docker network cleanup below.
workdir_cleanup_age: 24h
# Cadence for the idle cleanup pass. Besides the directories above, on runners that use
# docker it removes the per-job networks of jobs this runner did not live to tear down,
# which would otherwise hold a subnet of the daemon address pool until the host is rebuilt.
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
# Scripts run inside the job environment before the job's first step and after its last
# one, the equivalent of GitHub's ACTIONS_RUNNER_HOOK_JOB_STARTED and
# ACTIONS_RUNNER_HOOK_JOB_COMPLETED, which are read when these are unset. The paths are
# resolved inside the job environment. Either one failing fails the job.
# Full guide: docs/job-hooks.md
hooks:
job_started: ''
job_completed: ''
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. A trailing slash is optional.
# 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
# Serve the actions cache service v2 API, used by actions/[email protected] and later. Those actions
# refuse any host they do not take for GitHub, so reaching it means editing that check out of
# the action's own bundle, keeping the untouched copy beside it. The same edit lets the stock
# upload-artifact and download-artifact work here. A bundle that does not match is left alone.
v2: true
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. An auto-created
# network is labelled com.gitea.runner.uuid=<this runner's uuid>, which is how the idle
# cleanup tells its own leftovers apart from those of other runners on the same daemon.
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, for example:
# options: --add-host=my.gitea.url:host-gateway
# A volume declared here replaces the one the runner mounts on the same container path, so the
# tool cache can be kept on the host. Its source must also be allowed by valid_volumes below:
# options: --volume /host/toolcache:/opt/hostedtoolcache
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.
# Two exceptions: an image pinned by digest (image@sha256:...) cannot change, so it is never
# re-pulled, and a pull that fails while a copy is already on the host does not fail the job,
# which runs on that copy with a warning in its log.
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
```
+85
View File
@@ -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, so the runner takes an advisory lock on `<runner.file>.lock` and refuses to start when another process already holds it. The lock is released by the operating system when the process exits, including after a crash. When the lock cannot be created at all — for example on a read-only mount — the runner logs a warning and starts without the guard.
## 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:nightly
```
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).
+53
View File
@@ -0,0 +1,53 @@
---
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.
## 3.0
### Breaking
- **Container options from workflows are filtered.** While `container.privileged` is disabled, options in a job's `container.options` that would escape the container are stripped with a warning: `--pid`, `--ipc`, `--uts`, `--cgroupns`, `--userns`, `--cap-add`, `--security-opt`, `--device`, `--device-cgroup-rule`, `--gpus`, `--volumes-from`, `--runtime`, `--cgroup-parent`, `--sysctl`. Workflows that relied on them need a runner with privileged mode enabled.
- **One process per registration file.** The daemon takes an advisory lock on `<runner.file>.lock` and refuses to start when another process already uses that file. Setups that started several runners from one directory must give each its own `runner.file` (or its own working directory).
- **Cache service v2 is served and enabled by default.** `actions/[email protected]` and later, and the stock `actions/upload-artifact` / `download-artifact` from `v4.4.0` on, reach it through a patch the runner applies to the action's own bundle. Jobs are pointed at the runner's cache server as their results service, which then forwards artifact calls to Gitea. Set `cache.v2: false` to keep to v1.
### Also new
- [Job hooks](hooks/job-hooks.md) (`runner.hooks.job_started` / `job_completed`) running inside the job environment.
- [Proxy variables](proxy.md) propagated to jobs, service containers and Dockerfile action builds.
- Secrets are masked in the log even when a job prints them in an encoded form.
- The tool cache can be relocated, and runner-managed paths can be mounted over, via `container.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, or a [job hook](hooks/job-hooks.md), 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.
## 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.
@@ -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.
@@ -0,0 +1,107 @@
---
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.
## Cache service v2
`actions/[email protected]` and later can use the *cache service v2* API. The runner serves it from the same store as v1, on by default, and it also works behind a shared cache server. Turn it off with:
```yaml
cache:
v2: false
```
Those actions refuse any host they do not take for GitHub. Rather than misreport the server URL, the runner edits that check out of the action's own JavaScript bundle and keeps the untouched copy beside it; a bundle it does not recognise is left alone and keeps to v1. The same edit lets the stock `actions/upload-artifact` and `actions/download-artifact` work from `v4.4.0` on, without the `gitea-upload-artifact` fork.
Because `ACTIONS_RESULTS_URL` names one origin that has to serve the whole results API, the cache server also forwards the artifact half of it to the Gitea instance the job belongs to. Jobs are therefore pointed at the cache server, which is what makes clients such as `docker buildx` find the cache service instead of receiving a 404 from Gitea.
## 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:3
```
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,138 @@
---
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. |
| `hooks.job_started` / `hooks.job_completed` | | scripts run inside the job environment, see [Job hooks](hooks/job-hooks.md). |
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 left behind by jobs the runner did not live to tear down are removed. They are recognised by the `com.gitea.runner.uuid` label carrying this runner's uuid, so leftovers of other runners on the same daemon are left alone. Without this, each leaked network keeps holding a subnet of the daemon's address pool.
## `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. |
| `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. |
| `v2` | `true` | serve the cache service v2 API used by `actions/[email protected]` and later. |
## `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`. A volume declared here replaces the one the runner mounts on the same container path, which is how the tool cache can be kept on the host (`--volume /host/toolcache:/opt/hostedtoolcache`); its source must also be allowed by `valid_volumes`. |
| `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. Images pinned by digest are never re-pulled, and a failed pull with a local copy available only warns. |
| `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`. |
:::note Privileged mode and workflow container options
A workflow's own `jobs.<job_id>.container.options` are untrusted input. While `container.privileged` is disabled, the options that would break out of the container are stripped with a warning in the job log: `--pid`, `--ipc`, `--uts`, `--cgroupns`, `--userns`, `--cap-add`, `--security-opt`, `--device`, `--device-cgroup-rule`, `--gpus`, `--volumes-from`, `--runtime`, `--cgroup-parent` and `--sysctl`. They are honoured once privileged mode is enabled, because the operator has then opted into host access.
:::
## `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,74 @@
---
sidebar_position: 1
---
# Job hooks
Job hooks are operator-provided scripts that run **inside the job environment**, before the job's first step and after its last one. They are the equivalent of GitHub's [job hooks](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/run-scripts):
```yaml
runner:
hooks:
job_started: /hooks/started.sh
job_completed: /hooks/completed.sh
```
| Setting | Runs |
| --- | --- |
| `runner.hooks.job_started` | before the job's first step, before any action is downloaded |
| `runner.hooks.job_completed` | after the job's last post step, while the job environment is still up |
`ACTIONS_RUNNER_HOOK_JOB_STARTED` and `ACTIONS_RUNNER_HOOK_JOB_COMPLETED` are read from the runner's environment (`runner.envs`, `runner.env_file`) when the settings are unset, so a configuration carried over from `actions/runner` keeps working. The settings take precedence. A workflow cannot point the runner at a different hook: those variables are only read from the runner's own environment, never from the job's.
Both hooks are **synchronous** and block the job while they run, and a non-zero exit from either one fails the job. There is no `continue-on-error` and no per-hook timeout — the job's `runner.timeout` is the only bound. Run anything long in the background from within the hook.
Use them for per-job setup that no workflow should have to carry: registry logins, mirror configuration, or masking runner-wide secrets with `::add-mask::`.
## Where they run
The hooks run where the job's steps run: inside the job container, or on the host in host mode. The paths are resolved *there*, so the script has to exist in the job image or on the host — a path that only exists on the runner host is not visible to a containerized job. For host-wide cleanup after the job environment is gone, use the [post-task script](post-task-script.md) instead.
:::note
This is a deliberate difference from `actions/runner`, which runs its job hooks on the host, outside any container the job declares. Running them where the steps run is what lets a hook prepare the environment the steps actually see.
:::
The script is run according to its extension:
| Extension | Command |
| --- | --- |
| `.sh` | `bash -e <path>` |
| `.ps1` | `pwsh -command . '<path>'` |
| anything else | the file itself, which needs its own shebang and executable bit |
As on GitHub, the shell flags applied to `run:` steps are **not** applied to a hook — set `pipefail` or anything else you want inside the script.
A hook path that does not exist inside the job environment fails the job with `No such file or directory`, naming the path.
### Docker-in-Docker and Docker-out-of-Docker
The hook is executed and its files are exchanged over the Docker API, addressed by container ID, so no path is translated between the runner and the daemon. Both setups work unchanged, but they differ in where the hook file has to be:
- **DinD** — the daemon has its own filesystem. Bake the hook into the job image; a path from the runner's filesystem is not visible to it.
- **DooD** — the job container is created by the host's daemon, so a bind mount in `container.options` is resolved against the **host**, not against the runner container. Either bake the hook into the job image, or mount a host directory and add it to `container.valid_volumes`.
## Environment
A hook sees the job's environment: the workflow, job and `container:` `env:`, the runner's `envs`, and the `GITHUB_*` context variables, with the same masking applied to its output as to a step's. The step-specific ones (`GITHUB_ACTION`, `GITHUB_OUTPUT`, `GITHUB_STATE`) are not set — a hook is not a step, so `::save-state::` and `::set-output::` have nowhere to go.
Its stdout is part of the job log, inside a collapsible group, and is scanned for workflow commands: `::add-mask::` registers a value to be masked for the rest of the job, `::set-env::` and `::add-path::` apply to the steps that follow.
`$GITHUB_ENV` and `$GITHUB_PATH` point at files that are read back after the hook exits, so the file-command form works too:
```bash
#!/bin/bash
echo "REGISTRY_TOKEN=$(fetch-token)" >> "$GITHUB_ENV"
echo "/opt/tooling/bin" >> "$GITHUB_PATH"
```
Both files are the hook's own, separate from the per-step ones, so nothing a hook writes is truncated by the first step.
## Recommendations
- Keep hooks **fast** and return the right exit code: they are on the critical path of every job, and nothing bounds them.
- Use **idempotent** operations, and expect `job_completed` to run after success, failure, and cancellation alike.
- Mask anything secret the hook prints or exports with `::add-mask::`.
@@ -0,0 +1,101 @@
---
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.
- For work that has to happen inside the job environment, use [job hooks](job-hooks.md) instead.
@@ -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 `3.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), [job hooks](hooks/job-hooks.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` and `3.0.0` both contain breaking changes.
@@ -0,0 +1,153 @@
---
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=3.0.2 # any 3.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).
Environment variables for the process — most importantly [proxy variables](../proxy.md) — belong in `Environment=` lines or a drop-in file, not in the runner config.
## 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`.
`latest` is the newest release and the `3` tag follows the newest `3.x` release; `nightly` is built from the `main` branch, and every release is also tagged with its exact version.
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 |
| --- | --- | --- | --- | --- |
| `latest`, `3`, `3.0`, `<version>` | `alpine` | none, you provide one | `tini` | `root` |
| `latest-dind`, `3-dind` | `docker:dind` | bundled, needs `--privileged` | `s6` | `root` |
| `latest-dind-rootless`, `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:3
```
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:3-dind
```
`s6` starts `dockerd` first and the runner service waits for it before registering. Use `3-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:3 gitea-runner generate-config > config.yaml
```
## docker compose
```yaml
services:
runner:
image: docker.io/gitea/runner:3
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:3-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: 3.0.2
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,46 @@
---
sidebar_position: 6
---
# Proxies
The runner reads the usual proxy variables from its own environment and passes them on to every job:
```sh
http_proxy=http://proxy.example:3128
https_proxy=http://proxy.example:3128
no_proxy=gitea.internal,.example.local
```
Set them where the process is started — `Environment=` in a systemd unit, `docker run -e`, or `env:` in Kubernetes. They are used for the runner's own requests and are given to jobs in both lower and upper case.
## What is exempted automatically
These are added to `no_proxy` for jobs, so they are always reached directly:
- the cache server
- `localhost`, `127.0.0.1` and `::1`
- the job's service containers
- the Docker daemon, when it is reached over `tcp://`
The Gitea instance is **not** added. Add it to `no_proxy` yourself if it should be reached directly.
## Overriding per job or per runner
| Scope | Where to set it |
| --- | --- |
| one step | the step's `env:` |
| one job | the job's `container.env` |
| the whole runner | `runner.envs` in the config; a `no_proxy` set there is added to the list above instead of replacing it |
Setting proxy variables at workflow or job level (outside `container.env`) has no effect.
## The Docker daemon needs its own setting
Images are pulled by the daemon, not by the runner, so the daemon needs its own proxy configuration. In the `dind` images the daemon shares the container's environment and picks the variables up; for any other daemon see [the Docker documentation](https://docs.docker.com/engine/daemon/proxy/). The runner logs a warning at startup when it has a proxy configured and the daemon does not.
Dockerfile actions are built with these variables passed as build arguments, so their `RUN` steps can reach the network.
## Credentials in proxy URLs
A password inside a proxy URL is masked in job logs, but any step can still read it: the step is given the proxy URL in its environment. Prefer a proxy that does not need credentials, or one that authenticates by source address.
@@ -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,276 @@
---
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.
# With `container.network` empty, each 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 docker daemon config.
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, along with
# the docker network cleanup below.
workdir_cleanup_age: 24h
# Cadence for the idle cleanup pass. Besides the directories above, on runners that use
# docker it removes the per-job networks of jobs this runner did not live to tear down,
# which would otherwise hold a subnet of the daemon address pool until the host is rebuilt.
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
# Scripts run inside the job environment before the job's first step and after its last
# one, the equivalent of GitHub's ACTIONS_RUNNER_HOOK_JOB_STARTED and
# ACTIONS_RUNNER_HOOK_JOB_COMPLETED, which are read when these are unset. The paths are
# resolved inside the job environment. Either one failing fails the job.
# Full guide: docs/job-hooks.md
hooks:
job_started: ''
job_completed: ''
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. A trailing slash is optional.
# 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
# Serve the actions cache service v2 API, used by actions/[email protected] and later. Those actions
# refuse any host they do not take for GitHub, so reaching it means editing that check out of
# the action's own bundle, keeping the untouched copy beside it. The same edit lets the stock
# upload-artifact and download-artifact work here. A bundle that does not match is left alone.
v2: true
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. An auto-created
# network is labelled com.gitea.runner.uuid=<this runner's uuid>, which is how the idle
# cleanup tells its own leftovers apart from those of other runners on the same daemon.
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, for example:
# options: --add-host=my.gitea.url:host-gateway
# A volume declared here replaces the one the runner mounts on the same container path, so the
# tool cache can be kept on the host. Its source must also be allowed by valid_volumes below:
# options: --volume /host/toolcache:/opt/hostedtoolcache
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.
# Two exceptions: an image pinned by digest (image@sha256:...) cannot change, so it is never
# re-pulled, and a pull that fails while a copy is already on the host does not fail the job,
# which runs on that copy with a warning in its log.
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, so the runner takes an advisory lock on `<runner.file>.lock` and refuses to start when another process already holds it. The lock is released by the operating system when the process exits, including after a crash. When the lock cannot be created at all — for example on a read-only mount — the runner logs a warning and starts without the guard.
## 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:3
```
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,53 @@
---
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.
## 3.0
### Breaking
- **Container options from workflows are filtered.** While `container.privileged` is disabled, options in a job's `container.options` that would escape the container are stripped with a warning: `--pid`, `--ipc`, `--uts`, `--cgroupns`, `--userns`, `--cap-add`, `--security-opt`, `--device`, `--device-cgroup-rule`, `--gpus`, `--volumes-from`, `--runtime`, `--cgroup-parent`, `--sysctl`. Workflows that relied on them need a runner with privileged mode enabled.
- **One process per registration file.** The daemon takes an advisory lock on `<runner.file>.lock` and refuses to start when another process already uses that file. Setups that started several runners from one directory must give each its own `runner.file` (or its own working directory).
- **Cache service v2 is served and enabled by default.** `actions/[email protected]` and later, and the stock `actions/upload-artifact` / `download-artifact` from `v4.4.0` on, reach it through a patch the runner applies to the action's own bundle. Jobs are pointed at the runner's cache server as their results service, which then forwards artifact calls to Gitea. Set `cache.v2: false` to keep to v1.
### Also new
- [Job hooks](hooks/job-hooks.md) (`runner.hooks.job_started` / `job_completed`) running inside the job environment.
- [Proxy variables](proxy.md) propagated to jobs, service containers and Dockerfile action builds.
- Secrets are masked in the log even when a job prints them in an encoded form.
- The tool cache can be relocated, and runner-managed paths can be mounted over, via `container.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, or a [job hook](hooks/job-hooks.md), 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.
## 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.
@@ -0,0 +1,9 @@
{
"runner": [
{
"type": "doc",
"id": "index",
"label": "Gitea Runner"
}
]
}
@@ -0,0 +1,9 @@
{
"runner": [
{
"type": "doc",
"id": "index",
"label": "Gitea Runner"
}
]
}
@@ -0,0 +1,40 @@
{
"runner": [
{
"type": "doc",
"id": "index",
"label": "What is the Gitea Runner?"
},
{
"type": "category",
"label": "Installation",
"collapsed": false,
"items": [
"installation/binary",
"installation/docker",
"installation/kubernetes"
]
},
"registration",
"configuration",
"labels",
"cache",
{
"type": "category",
"label": "Hooks",
"items": [
"hooks/post-task-script"
]
},
"monitoring",
"upgrade",
{
"type": "category",
"label": "Reference",
"items": [
"reference/cli",
"reference/config-example"
]
}
]
}
@@ -0,0 +1,42 @@
{
"runner": [
{
"type": "doc",
"id": "index",
"label": "What is the Gitea Runner?"
},
{
"type": "category",
"label": "Installation",
"collapsed": false,
"items": [
"installation/binary",
"installation/docker",
"installation/kubernetes"
]
},
"registration",
"configuration",
"labels",
"cache",
"proxy",
{
"type": "category",
"label": "Hooks",
"items": [
"hooks/job-hooks",
"hooks/post-task-script"
]
},
"monitoring",
"upgrade",
{
"type": "category",
"label": "Reference",
"items": [
"reference/cli",
"reference/config-example"
]
}
]
}
+4 -2
View File
@@ -1,4 +1,6 @@
[ [
"1.0.8", "3",
"0.2.11" "2",
"1",
"0"
] ]
+45
View File
@@ -0,0 +1,45 @@
// Sidebar of the runner documentation (develop version).
// Released versions carry their own copy in
// runner-docs_versioned_sidebars/version-<version>-sidebars.json.
module.exports = {
runner: [
{
type: 'doc',
id: 'index',
label: 'What is the Gitea Runner?',
},
{
type: 'category',
label: 'Installation',
collapsed: false,
items: [
'installation/binary',
'installation/docker',
'installation/kubernetes',
],
},
'registration',
'configuration',
'labels',
'cache',
'proxy',
{
type: 'category',
label: 'Hooks',
items: [
'hooks/job-hooks',
'hooks/post-task-script',
],
},
'monitoring',
'upgrade',
{
type: 'category',
label: 'Reference',
items: [
'reference/cli',
'reference/config-example',
],
},
],
};
+196
View File
@@ -0,0 +1,196 @@
#!/bin/bash
#
# Regenerates the generated pages of the runner documentation from the runner
# source: the command line reference and the example configuration file.
#
# Usage:
#
# ./update_runner_docs.sh develop docs, from the main branch
# ./update_runner_docs.sh --released every documented release series
# ./update_runner_docs.sh <git ref> <dir> one ref into one directory
#
# --released needs no version list: the documented series are the
# runner-docs_versioned_docs/version-<series>/reference directories, and the tag
# each one is generated from is the newest stable v<series>.x.y tag of
# gitea/runner, looked up through the Gitea API. A new series is documented by
# running `pnpm run docusaurus docs:version:runner-docs <series>`, nothing here
# has to be edited.
set -euo pipefail
RUNNER_REMOTE="${RUNNER_REMOTE:-https://gitea.com/gitea/runner.git}"
RUNNER_API="${RUNNER_API:-https://gitea.com/api/v1/repos/gitea/runner}"
VERSIONED_DOCS="runner-docs_versioned_docs"
SRC_DIR=".tmp/upstream-runner"
BIN="$PWD/.tmp/gitea-runner"
TAGS_FILE=".tmp/runner-tags.txt"
mkdir -p .tmp
# prints every tag name of the runner repository, one per line
list_runner_tags() {
local page=1 names
while :; do
names="$(curl --silent --show-error --fail \
"$RUNNER_API/tags?limit=50&page=$page" |
grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d '"' -f 4)"
[ -n "$names" ] || return 0
printf '%s\n' "$names"
page=$((page + 1))
done
}
# latest_stable_tag <series>, e.g. "3" -> "v3.0.2", empty if the series has no
# release yet. Pre-release tags (v3.0.0-rc1) are ignored on purpose.
latest_stable_tag() {
local series="$1"
if [ ! -s "$TAGS_FILE" ]; then
list_runner_tags > "$TAGS_FILE"
fi
grep -E "^v${series}\.[0-9]+\.[0-9]+$" "$TAGS_FILE" |
sed 's/^v//' |
sort -t . -k1,1n -k2,2n -k3,3n |
tail -n 1 |
sed 's/^/v/'
}
# checkout_runner <git ref>, builds the runner binary at $BIN
checkout_runner() {
local ref="$1"
if [ -d "$SRC_DIR/.git" ]; then
git -C "$SRC_DIR" remote set-url origin "$RUNNER_REMOTE"
else
rm -rf "$SRC_DIR"
git init --quiet "$SRC_DIR"
git -C "$SRC_DIR" remote add origin "$RUNNER_REMOTE"
fi
git -C "$SRC_DIR" fetch --quiet --depth 1 origin "$ref"
git -C "$SRC_DIR" checkout --quiet --detach FETCH_HEAD
(cd "$SRC_DIR" && go build -o "$BIN" .)
}
# the commands the reference documents, in the order they are presented
COMMANDS=(register daemon exec cache-server generate-config bug-report)
# one short paragraph per command, printed above its help output
describe_command() {
case "$1" in
register)
echo '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).'
;;
daemon)
echo '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.'
;;
exec)
echo '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.'
;;
cache-server)
echo '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).'
;;
generate-config)
echo 'Prints the commented example configuration on stdout, which is the starting point for a config file: `gitea-runner generate-config > config.yaml`.'
;;
bug-report)
echo 'Prints the runner version, Go version, OS/architecture and CPU count, for pasting into an issue.'
;;
esac
}
# generate_reference <git ref> <target directory>
generate_reference() {
local ref="$1" target="$2" cmd
mkdir -p "$target"
checkout_runner "$ref"
{
cat <<'EOF'
---
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.
EOF
printf '## gitea-runner\n\n```text\n'
"$BIN" --help
printf '```\n'
for cmd in "${COMMANDS[@]}"; do
printf '\n## %s\n\n' "$cmd"
describe_command "$cmd"
printf '\n```text\n'
"$BIN" "$cmd" --help
printf '```\n'
done
} > "$target/cli.md"
{
cat <<'EOF'
---
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
EOF
"$BIN" generate-config
printf '```\n'
} > "$target/config-example.md"
echo "wrote $target/cli.md and $target/config-example.md from $RUNNER_REMOTE@$ref"
}
# regenerates every documented release series from its newest stable tag
generate_released() {
local dir series tag found=0
for dir in "$VERSIONED_DOCS"/version-*/reference; do
[ -d "$dir" ] || continue
found=1
series="${dir#"$VERSIONED_DOCS"/version-}"
series="${series%/reference}"
tag="$(latest_stable_tag "$series")"
if [ -z "$tag" ]; then
echo "no stable v$series tag in $RUNNER_API, skipping $dir" >&2
continue
fi
generate_reference "$tag" "$dir"
done
if [ "$found" -eq 0 ]; then
echo "no $VERSIONED_DOCS/version-*/reference directory to regenerate" >&2
exit 1
fi
}
case "${1:-}" in
--released)
generate_released
;;
-h | --help)
sed -n '3,17s/^#\{1,2\} \{0,1\}//p' "$0"
;;
*)
generate_reference "${1:-main}" "${2:-runner-docs/reference}"
;;
esac