Files
runner/README.md
T
silverwind f2fbd67962 enhance: support Docker on FreeBSD and restructure the README (#1367)
1. Restructures the README into installation, quickstart, deployment, configuration, jobs and operation, and shortens it to user-facing prose. Option details are left to `config.example.yaml` and `docs/`, existing heading anchors are kept where possible.
2. FreeBSD builds used the Docker stub, so `docker://` labels, services and Docker actions failed there. The Docker code works on FreeBSD against a remote daemon, so it is enabled, and host jobs report `runner.os` as `FreeBSD`.
3. `make build` on Windows now writes `gitea-runner.exe`, taking the suffix from `go env GOEXE`.

Reviewed-on: https://gitea.com/gitea/runner/pulls/1367
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-11-03 02:42:25 +00:00

14 KiB

Gitea Runner

Gitea Runner runs the jobs of Gitea Actions workflows. Register it with a Gitea instance to run jobs in Docker containers, Kubernetes pods or directly on the host.

Installation

Prerequisites

  • Linux, macOS or Windows, or FreeBSD.
  • git 2.34.1 or newer.
  • docker, or podman with podman.socket enabled, for docker labels and Docker actions. See Docker's install instructions.

In the job image, or on the host for host jobs:

  • bash or sh, or pwsh or powershell on Windows.
  • node for JavaScript actions and hashFiles().
  • git 2.34.1 or newer for builtin:checkout.
  • git-lfs for builtin:checkout with lfs: true.
  • ssh for builtin:checkout with ssh-key.

Kubernetes mode needs a few more tools in job images.

Download pre-built binary

Download the binary for your platform from dl.gitea.com.

Build from source

make build

Build a docker image

make docker

Quickstart

Enable Actions in the configuration file of your Gitea instance:

[actions]
ENABLED=true

Register

./gitea-runner register

It asks for:

  1. The Gitea instance URL. Use its ROOT_URL, not localhost or 127.0.0.1.
  2. A runner token, from /admin/actions/runners on your instance.
  3. A runner name, the hostname when left blank.
  4. The runner labels, the defaults when left blank.

To register without prompts:

./gitea-runner register --instance https://gitea.example.com --token <token> --no-interactive

Run

./gitea-runner daemon

Deployment

Windows service

Register the runner first, keeping gitea-runner.exe, config.yaml and the .runner file in one directory. Then install and start the service from an elevated Command Prompt:

sc.exe create gitea-runner binPath= "\"C:\gitea-runner\gitea-runner.exe\" daemon --config \"C:\gitea-runner\config.yaml\"" start= auto
sc.exe start gitea-runner

The service logs to gitea-runner.log next to the executable. Its account needs write access to that directory and access to the tools jobs use.

sc.exe stop gitea-runner lets running jobs finish within runner.shutdown_timeout. To allow the same on shutdown, raise the service's preshutdown timeout above it, here to six minutes. This takes effect after the next reboot:

reg.exe add HKLM\SYSTEM\CurrentControlSet\Services\gitea-runner /v PreshutdownTimeout /t REG_DWORD /d 360000 /f

Run with docker

The recommended Docker-in-Docker image runs its own Docker daemon for jobs. Jobs never get the host's Docker socket, which is equivalent to root access on the host.

docker run --privileged -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> \
  -v "$PWD/data:/data" -v runner-docker:/var/lib/docker --name my_runner gitea/runner:nightly-dind

The volumes keep the registration and the image cache across container recreation. The image reads more variables, such as GITEA_RUNNER_LABELS and CONFIG_FILE, see scripts/run.sh and the Docker example.

Image flavours

Tag Docker daemon for jobs
latest-dind (recommended) Bundled, needs --privileged.
latest-dind-rootless Bundled, runs as UID 1000 for stronger isolation. Needs --privileged and rootless Docker support on the host. Its image cache is in /home/rootless/.local/share/docker.
latest None. Mount the host's /var/run/docker.sock, or set DOCKER_HOST.

Versioned and nightly tags take the same suffixes, like nightly-dind. The images target Docker, not Podman.

Example deployments

See the examples directory for more deployments, among them systemd, Kubernetes and VMs.

Configuration

The runner reads an optional YAML file:

./gitea-runner config init             # write config.yaml, with no option set
./gitea-runner config generate | less  # read what the options do
./gitea-runner -c config.yaml daemon   # -c also works on register and cache-server

config generate prints config.example.yaml, which documents every option. Copy the lines you need into your file and uncomment them.

Editing a config file

config edits a file in place and keeps its comments, which suits provisioning scripts:

./gitea-runner config set runner.capacity 4
./gitea-runner config set runner.envs.MY_VAR value
./gitea-runner config add runner.labels 'ubuntu:docker://node:22'
./gitea-runner config remove runner.labels 'ubuntu:docker://node:22'
./gitea-runner config get runner.labels

Keys are dotted YAML paths. Unknown keys and invalid values are refused without touching the file. Without -c, it uses config.yaml from the working directory, then from the binary's directory.

Environment variables

Config values can read the runner's environment as ${NAME}, for example capacity: ${RUNNER_CAPACITY}. An unset variable fails the load, and $${NAME} stays a literal ${NAME}.

The override variables of earlier releases, such as GITEA_RUNNER_CAPACITY, are gone. Set those options in the file instead.

Labels

Labels decide which jobs a runner accepts and how it runs them. A job's runs-on is matched against the label names, and the first match picks where it runs.

A label is written as <name>[:<schema>[:<args>]], with one of these schemas:

  • docker://<image> runs the job in a container of <image>.
  • kubernetes://<image> runs it as a Kubernetes pod, see docs/kubernetes.md.
  • host, the default, runs its steps directly on the runner's machine.

With these labels, runs-on: ubuntu-latest runs in a container and runs-on: macos on the host:

ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest,macos:host

gitea/runner-images has suitable images. Jobs matching no label run in runner.default_image, or on the host when docker is unavailable.

Host jobs still need a Docker daemon for Docker actions and docker:// steps. Windows hosts do not support those, as on GitHub. On Windows, a step's background processes end with the job unless it opts out, see host mode.

Registration vs config labels

Labels are set at registration, with --labels or the prompt. To change them later, use one of these, the leftmost set one wins:

daemon --labels / GITEA_RUNNER_LABELS   >   runner.labels in the config file   >   labels in the .runner file

Changed labels are saved to the .runner file and sent to Gitea on startup. When runner.labels is set, register uses it and ignores --labels.

Tool cache

Setup actions like setup-go install tools into RUNNER_TOOL_CACHE. runner.tool_cache_mode selects what backs it:

Mode Tool cache
none (default) Provided by the job image. Versions it lacks are downloaded in every job.
shared One volume for all jobs. Only safe with runner.capacity: 1.

To ship tools in a job image, install them to /opt/hostedtoolcache/<tool>/<version>/<arch> with an empty <arch>.complete file next to it, as the -full runner images do:

RUN GO=$(curl -fsSL 'https://go.dev/dl/?mode=json' | grep -oP '"version": "\Kgo1\.26\.[0-9]*' | head -1); \
  DIR="/opt/hostedtoolcache/go/${GO#go}/x64" && \
  mkdir -p "$(dirname "$DIR")" && \
  curl -fsSL "https://dl.google.com/go/${GO}.linux-amd64.tar.gz" | tar -xz -C /tmp && \
  mv /tmp/go "$DIR" && \
  touch "${DIR}.complete"

Proxy

Set the proxy in the runner's environment, for example with systemd Environment= or docker run -e:

http_proxy=http://proxy.example:3128
https_proxy=http://proxy.example:3128
no_proxy=gitea.internal,.example.local

The runner uses it and passes it on to jobs. Jobs always reach the cache server, localhost, their service containers and a tcp:// Docker daemon directly. Add Gitea to no_proxy if it should be reached directly too.

To change the proxy for jobs, use runner.envs, a step's env: or $GITHUB_ENV. Workflow and job level env: have no effect on it.

The Docker daemon pulls images with its own proxy settings. The dind images pass the variables above to it, for other daemons see the Docker documentation.

A password in the proxy URL is hidden in logs, but every step can read it.

Jobs

Service containers

When a service declares a healthcheck, in its image or options, the job waits for it to become healthy:

services:
  postgres:
    image: postgres:18
    options: >-
      --health-cmd pg_isready
      --health-interval 5s
      --health-retries 10

A service that turns unhealthy, or is not healthy within container.service_ready_timeout, fails the job. Steps reach a service by its id and port, like psql -h postgres -p 5432.

Unlike GitHub, jobs running on the host start no services. Give them a container: to use services.

Docker from a job (GITEA_DOCKER_WORKSPACE)

Containers a job starts can bind-mount the workspace by its path, like docker run -v "$PWD:/src". Where the Docker daemon sees another path, for example a remote DOCKER_HOST, prefix the bind with GITEA_DOCKER_WORKSPACE:

volumes:
  - ${GITEA_DOCKER_WORKSPACE:-.}/data:/app/data

In most Linux container jobs, the containers, networks and volumes a job creates are removed when it ends, docker compose services included. Set container.sweep: false if jobs deploy services to the runner's Docker daemon.

Built-in actions (uses: builtin:<name>)

uses: builtin:checkout checks out a repository without downloading actions/checkout or needing Node in the job image, see docs/builtin-actions.md.

Caching (actions/cache)

Each runner runs its own cache server. As on GitHub, a job restores caches of its own ref, its pull request's base branch and the default branch.

Sharing a cache between runners

Run one gitea-runner cache-server and point every runner at it with the same secret, for example from openssl rand -hex 32:

# cache-server.yaml
cache:
  dir: /data/actcache
  port: 8088
  external_secret: "<secret>" # or external_secret_file: /path/to/secret
gitea-runner -c cache-server.yaml cache-server
# each runner's config
cache:
  external_server: "http://<cache-server-host>:8088/"
  external_secret: "<secret>"

Runners can also share one cache.dir on a file system with working file locks, at the cost of slower requests.

Storing the cache in an S3-compatible bucket

Runners can share a cache in an S3-compatible bucket, such as AWS S3, MinIO or RustFS, without a cache server:

cache:
  s3:
    endpoint: "http://minio:9000"
    region: "us-east-1"
    bucket: "runner-cache"
    prefix: "gitea-runner-cache"
    path_style: true
    access_key_file: "/run/secrets/cache-s3-access-key"
    secret_key_file: "/run/secrets/cache-s3-secret-key"

Eviction does not apply to the bucket. Add lifecycle rules that expire old caches and abort incomplete multipart uploads.

Eviction

Entries unused for cache.retention (default 168h) are removed. A repository over cache.repo_size_limit (default 10GB) loses its least recently used entries. See config.example.yaml for more limits.

Cache service v2

The runner serves the cache service v2 API used by current actions/cache releases. It patches them to use it on Gitea, which also makes the stock actions/upload-artifact and actions/download-artifact v4 work. Artifact calls then go through the cache server, so jobs must be able to reach it.

Job hooks (runner.hooks.job_started, runner.hooks.job_completed)

Hook scripts run where the job's steps run, before the first step and after the last. They match GitHub's ACTIONS_RUNNER_HOOK_JOB_STARTED and ACTIONS_RUNNER_HOOK_JOB_COMPLETED.

Use them for per-job setup no workflow should carry, like registry logins or masking runner-wide secrets. They block the job, and a non-zero exit fails it. See docs/job-hooks.md.

Secret masking

Secrets and ::add-mask:: values are hidden in job logs, local job logs, job summaries and container names. As on GitHub, a job output containing one is dropped with a warning.

Operation

Idle cleanup

While idle, the runner removes leftovers of earlier jobs every runner.idle_cleanup_interval (default 10m):

  • Workspaces older than runner.workdir_cleanup_age (default 24h), when container.bind_workdir is on. This assumes no other runner shares container.workdir_parent.
  • Unused Docker networks and volumes of jobs the runner did not tear down.
  • Kubernetes job pods older than runner.workdir_cleanup_age, with their Secrets.

Set either option to 0 to turn cleanup off.

Post-task script (runner.post_task_script)

A host script that runs after each task's cleanup, for housekeeping like Docker pruning. The runner takes no new jobs until it exits or hits runner.post_task_script_timeout (default 5m). Its output goes to the runner log.

On Windows, use an .exe, .bat or .cmd file, PowerShell scripts are not supported. See docs/post-task-script.md.

Local job logs (log.job.dir)

Set log.job.dir to keep a copy of every task's log, masked as in Gitea, with the job's result on the last line. Copies are kept for log.job.retention (default 168h), so keep it above runner.timeout. Only the runner's user can read them.

OpenTelemetry (OTEL_*)

Set OTEL_EXPORTER_OTLP_ENDPOINT in the runner's environment to send job traces and runner metrics to an OpenTelemetry collector. See docs/telemetry.md.