Install with Docker
The official images are published on Docker Hub 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
Section titled “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
Section titled “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:3This 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
Section titled “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:3-dinds6 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
Section titled “Volumes”Two different pieces of state are worth persisting, and neither implies the other:
/datais the runner’s working directory. It holds the.runnerregistration 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: fordindit is/var/lib/dockerinside the container, fordind-rootlessit is/home/rootless/.local/share/docker. Give it its own volume, or every new container re-pulls the job images.
Entrypoint environment variables
Section titled “Entrypoint environment variables”The entrypoint (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 |
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.
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:3 gitea-runner generate-config > config.yamldocker compose
Section titled “docker compose”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.sockWhen 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: trueThe rootless Docker-in-Docker variant needs a few extra options:
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=65520Cache from a dockerized runner
Section titled “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.