跳到主要内容
版本:develop

Install with Docker

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

TagBase imageDocker daemonSupervisorRuns as
nightly, latest, <version>alpinenone, you provide onetiniroot
nightly-dind, latest-dinddocker:dindbundled, needs --privilegeds6root
nightly-dind-rootless, latest-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: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:

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) 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:nightly gitea-runner generate-config > config.yaml

docker compose

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:

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: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.

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