bircni aa7a29a157
release-tag / goreleaser (push) Skipped
release-tag / release-image (map[platforms:linux/amd64,linux/arm64 tag_suffix:-dind target:dind]) (push) Skipped
release-tag / release-image (map[platforms:linux/amd64,linux/arm64 tag_suffix:-dind-rootless target:dind-rootless]) (push) Skipped
release-tag / release-image (map[platforms:linux/amd64,linux/arm64,linux/riscv64,linux/s390x tag_suffix: target:basic]) (push) Skipped
release-nightly / goreleaser (push) Successful in 1m32s
release-nightly / release-image (map[platforms:linux/amd64,linux/arm64 tag_suffix:-dind target:dind]) (push) Successful in 4m37s
release-nightly / release-image (map[platforms:linux/amd64,linux/arm64 tag_suffix:-dind-rootless target:dind-rootless]) (push) Successful in 4m39s
checks / check and test (push) Successful in 5m43s
release-nightly / release-image (map[platforms:linux/amd64,linux/arm64,linux/riscv64,linux/s390x tag_suffix: target:basic]) (push) Successful in 7m31s
fix: guard status-check functions against a nil job context (#1092)
The `cancelled()`, `success()` and `failure()` expression functions dereferenced `Job.Status` unconditionally, so a nil `Job` context panicked the interpreter — which is why Gitea currently hands the runner a non-nil (empty) `JobContext` as a workaround. This routes all three through a `jobStatus()` helper that treats a nil `Job` as an empty status, keeping existing behaviour identical while removing the panic. Includes a regression test that panics on the old code and passes with the fix.

Related: https://github.com/go-gitea/gitea/pull/38495

Reviewed-on: #1092
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
2026-07-16 21:33:26 +00:00
2026-07-13 18:41:19 +00:00
2026-07-01 03:26:42 +00:00
2026-07-01 03:26:42 +00:00
2023-03-18 01:58:21 -04:00
2026-04-24 13:04:47 +02:00
2026-04-24 13:04:47 +02:00
2022-11-24 15:36:16 +08:00
2026-04-30 20:12:51 +00:00
2026-07-01 03:26:42 +00:00
2023-11-23 20:41:10 +00:00

Gitea Runner

Installation

Prerequisites

Docker Engine Community version is required for docker mode. To install Docker CE, follow the official install instructions.

Download pre-built binary

Visit here and download the right version for your platform.

Build from source

make build

Build a docker image

make docker

Quickstart

Actions are disabled by default, so you need to add the following to the configuration file of your Gitea instance to enable it:

[actions]
ENABLED=true

Register

./gitea-runner register

And you will be asked to input:

  1. Gitea instance URL, like http://192.168.8.8:3000/. You should use your gitea instance ROOT_URL as the instance argument and you should not use localhost or 127.0.0.1 as instance IP;
  2. Runner token, you can get it from http://192.168.8.8:3000/admin/actions/runners;
  3. Runner name, you can just leave it blank;
  4. Runner labels, you can just leave it blank.

The process looks like:

INFO Registering runner, arch=amd64, os=darwin, version=0.1.5.
WARN Runner in user-mode.
INFO Enter the Gitea instance URL (for example, https://gitea.com/):
http://192.168.8.8:3000/
INFO Enter the runner token:
fe884e8027dc292970d4e0303fe82b14xxxxxxxx
INFO Enter the runner name (if set empty, use hostname: Test.local):

INFO Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest):

INFO Registering runner, name=Test.local, instance=http://192.168.8.8:3000/, labels=[ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04 ubuntu-20.04:docker://docker.gitea.com/runner-images:ubuntu-20.04].
DEBU Successfully pinged the Gitea instance server
INFO Runner registered successfully.

You can also register with command line arguments.

./gitea-runner register --instance http://192.168.8.8:3000 --token <my_runner_token> --no-interactive

If the registry succeed, it will run immediately. Next time, you could run the runner directly.

Run

./gitea-runner daemon

Run with docker

docker run -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> -v /var/run/docker.sock:/var/run/docker.sock --name my_runner gitea/runner:nightly

Mount a volume on /data if you want the registration file and optional config to survive container recreation (see scripts/run.sh).

/data does not hold the image cache. It is the runner's working directory and contains only the .runner registration file and, optionally, your config file. Images pulled for jobs live in the Docker daemon's data root, which for the dind flavours is inside the container (/var/lib/docker, or /home/rootless/.local/share/docker for dind-rootless). To keep the image cache across restarts, give that path its own volume as well — otherwise every new container re-pulls the job images. With the basic flavour the images live on whichever daemon you point the runner at, so there is nothing extra to persist.

Image flavours

The image is published in three flavours, all built from the single multi-stage Dockerfile in this repository. They differ only in how a Docker daemon is made available to the jobs the runner executes; the gitea-runner binary inside them is identical.

Tag Build target Base image Docker daemon Process supervisor Runs as
latest (and <version>) basic alpine none — uses an external daemon you provide tini root
latest-dind dind docker:dind bundled, started inside the container s6 root (privileged)
latest-dind-rootless dind-rootless docker:dind-rootless bundled, started rootless inside the container s6 rootless (UID 1000)

latest — basic

The default flavour ships only the runner on a minimal Alpine base. It contains no Docker daemon of its own: jobs that use docker:// images need a daemon supplied from outside the container, typically by bind-mounting the host's socket:

docker run -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> \
  -v /var/run/docker.sock:/var/run/docker.sock --name my_runner gitea/runner:latest

tini is the entrypoint (it reaps zombie processes), and it just runs scripts/run.sh, which registers the runner on first start and then execs gitea-runner daemon. This flavour does not need --privileged. The trade-off is that jobs share the host's daemon, so they can see other containers and images on that daemon.

latest-dind — Docker-in-Docker

This flavour is based on the official docker:dind image and bundles its own Docker daemon, so it needs no external socket — only the --privileged flag that Docker-in-Docker requires:

docker run --privileged -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> \
  --name my_runner gitea/runner:latest-dind

Two processes have to run side by side here (the Docker daemon and the runner), so the entrypoint is the s6 supervision tree under scripts/s6 instead of tini. s6 starts dockerd, and the runner service waits for the daemon to come up (s6-svwait) before launching run.sh. Each container has a private daemon isolated from the host's, at the cost of running privileged.

latest-dind-rootless — rootless Docker-in-Docker

Same idea as dind, but built on docker:dind-rootless so the bundled daemon and the runner run as an unprivileged user (rootless, UID 1000) rather than root. DOCKER_HOST is preset to unix:///run/user/1000/docker.sock so the runner talks to the rootless daemon. This reduces the blast radius compared to the privileged dind flavour, but rootless Docker carries the usual rootless limitations (networking, cgroups, storage drivers, and some operations that need additional host configuration such as /etc/subuid / /etc/subgid mappings and unprivileged user-namespace support).

The UID is fixed at 1000. It comes from the rootless user baked into the upstream docker:dind-rootless base image, and the bundled daemon always listens on /run/user/1000/docker.sock inside the container, so running this flavour as a different user (--user 1001) does not work. If you need the runner to talk to a host rootless daemon that runs under some other UID, use the basic flavour instead and bind-mount that daemon's socket (see examples/vm/rootless-docker.md); pointing DOCKER_HOST at a host socket from inside dind-rootless will not work. Changing the UID otherwise means rebuilding the image from a base with a different rootless user.

Note on Podman: these images target the Docker daemon. The bundled dind/dind-rootless daemons are dockerd, not Podman, and the basic flavour expects a Docker-compatible socket. Running them under rootless Podman is not a supported configuration, though pointing the basic flavour at a Podman socket that emulates the Docker API may work for some workloads.

Configuration

The runner is configured with a YAML file. Generate a starting point (this matches what ships in the tree):

./gitea-runner generate-config > config.yaml

Pass it with -c / --config on any command that loads configuration (register, daemon, cache-server):

./gitea-runner -c config.yaml register
./gitea-runner -c config.yaml daemon
./gitea-runner -c config.yaml cache-server

Every option is described in config.example.yaml (the same content generate-config prints).

Without a config file

If you omit -c, built-in defaults apply (same as an empty YAML document).

Earlier releases let a small set of environment variables (GITEA_DEBUG, GITEA_TRACE, GITEA_RUNNER_CAPACITY, GITEA_RUNNER_FILE, GITEA_RUNNER_ENVIRON, GITEA_RUNNER_ENV_FILE) override parts of the default config. Those overrides have been removed — use a YAML config file for all settings instead. For the Docker images, the entrypoint still understands a separate set of variables (such as RUNNER_STATE_FILE); see scripts/run.sh and the container documentation below.

Labels

Labels decide which jobs a runner accepts and how it runs them. A job's runs-on 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:

<name>[:<schema>[:<args>]]
Part Meaning
name The name a workflow refers to in runs-on, e.g. ubuntu-latest.
schema Either docker or host. Defaults to host when omitted.
args Only used by the docker schema: the image to run the job in.

Two schemas are supported:

  • docker://<image> — the job runs inside a container created from <image>:

    ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest
    
  • host — the job's steps run directly on the machine the runner is on, using the tools installed there:

    macos:host
    

So with the labels

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

a workflow with runs-on: ubuntu-latest is executed in the runner-images:ubuntu-latest container, and one with runs-on: macos is executed 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.

Labels are chosen at registration time (--labels, or the interactive prompt) and can be changed afterwards by editing runner.labels in the config file, or in the Gitea UI under the runner's settings.

Registration vs config labels

If runner.labels is set in the YAML file, those labels are used during register and the --labels CLI flag is ignored.

The daemon command also accepts --labels (which defaults to the GITEA_RUNNER_LABELS environment variable), so the labels of an already registered runner can be changed without deleting its registration file. The most explicit source wins:

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

Whenever the resulting labels differ from the ones in the registration file, they are written back to it and re-declared to the Gitea instance on startup.

Note: A runner that only exposes host labels still needs access to a Docker daemon (e.g. a mounted /var/run/docker.sock) whenever a job uses a docker:// action or a service container. host labels only change where the job's own steps run; container-based steps and actions are still executed with Docker.

Caching (actions/cache)

Each runner starts its own cache server automatically. Cache entries are local to that runner — runners do not share a cache by default.

Shared cache across multiple runners

Run one dedicated gitea-runner cache-server that all runners point at.

  1. Create a config file for the cache server host:

    cache:
      dir: /data/actcache
      port: 8088
      external_secret: "replace-with-a-strong-random-secret"
    
  2. Start the server:

    gitea-runner -c cache-server-config.yaml cache-server
    
  3. On every runner:

    cache:
      external_server: "http://<cache-server-host>:8088/"
      external_secret: "replace-with-a-strong-random-secret"  # must match the server
    

Alternatively, mount the same NFS/CIFS share on every runner and point cache.dir at it — simpler, but with weaker isolation between repositories.

S3 / MinIO — mount object storage as a FUSE filesystem (e.g. s3fs or goofys) and set cache.dir to the mount point.

Flags --dir, --host, and --port on cache-server override the corresponding cache.* YAML keys; all other settings, including external_secret, require the config file.

Official Docker image

Besides GITEA_INSTANCE_URL and GITEA_RUNNER_REGISTRATION_TOKEN, the image entrypoint supports optional variables such as CONFIG_FILE (passed through as -c), GITEA_RUNNER_LABELS, GITEA_RUNNER_EPHEMERAL, GITEA_RUNNER_ONCE, GITEA_RUNNER_NAME, GITEA_MAX_REG_ATTEMPTS, RUNNER_STATE_FILE, and GITEA_RUNNER_REGISTRATION_TOKEN_FILE. See scripts/run.sh for exact behavior.

For a fuller container-oriented walkthrough, see examples/docker.

When container.bind_workdir is enabled, stale task workspace directories can be cleaned while the runner is idle:

  • directories older than runner.workdir_cleanup_age are removed (default: 24h; set 0 to disable)
  • cleanup runs every runner.idle_cleanup_interval (default: 10m; set 0 to disable)
  • only purely numeric subdirectories under container.workdir_parent are treated as task workspaces and may be removed
  • cleanup assumes container.workdir_parent is not shared across multiple runners

Post-task script (runner.post_task_script)

Optional host script that runs after each task's built-in cleanup (post-steps, container teardown, bind-workdir removal). Use it for extra machine housekeeping — Docker pruning, disk cleanup, and similar.

While the script runs, the runner stops task heartbeats and stays offline from Gitea's perspective until the script exits (or hits runner.post_task_script_timeout, default 5m). A script that blocks without exiting keeps the runner from taking new work for up to that timeout. Script output goes to the runner log, not the job log; a non-zero exit is warned but does not change the job result.

On Windows, use .exe, .bat, or .cmd paths; PowerShell (.ps1) is not supported yet as the configured path — wrap commands in a .cmd file instead.

See docs/post-task-script.md for lifecycle details, environment variables, timeout interaction, and platform notes.

Example Deployments

Check out the examples directory for sample deployment types.

Description
Official runner for Gitea
Readme MIT
39 MiB
v2.1.0
Latest
2026-07-16 14:38:08 +00:00
Languages
Go 86.8%
JavaScript 12.1%
Makefile 0.5%
Shell 0.5%
Dockerfile 0.1%