跳到主要内容
版本:2.x

Install with Docker

The official images are published on Docker Hub 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.

TagBase imageDocker daemonSupervisorRuns as
2, 2.3, <version>alpinenone, you provide onetiniroot
2-dind, 2.3-dinddocker:dindbundled, needs --privilegeds6root
2-dind-rootless, 2.3-dind-rootlessdocker:dind-rootlessbundled, rootlesss6rootless (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:

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:

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) understands:

VariableMeaning
GITEA_INSTANCE_URLinstance to register against, e.g. https://gitea.example.com/
GITEA_RUNNER_REGISTRATION_TOKENregistration token; unset before the daemon starts
GITEA_RUNNER_REGISTRATION_TOKEN_FILEfile to read the token from, for Docker/Kubernetes secrets
GITEA_RUNNER_NAMErunner name, defaults to the container hostname
GITEA_RUNNER_LABELSlabels, passed to both register and daemon
GITEA_RUNNER_EPHEMERALany non-empty value registers the runner as ephemeral
GITEA_RUNNER_ONCEany non-empty value runs a single job, then exits
GITEA_MAX_REG_ATTEMPTSregistration attempts before giving up, default 10
RUNNER_STATE_FILEregistration file name inside /data, default .runner
CONFIG_FILEconfig 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.

Mount the config file when you need one:

docker run -v $PWD/config.yaml:/config.yaml -e CONFIG_FILE=/config.yaml ...

A config file can be generated with the image itself:

docker run --rm --entrypoint="" docker.io/gitea/runner:2 gitea-runner generate-config > config.yaml

docker compose

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:

depends_on:
gitea:
condition: service_healthy
restart: true

The rootless Docker-in-Docker variant needs a few extra options:

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.

More deployment examples live in the examples directory of the runner repository.