Add runner 2 and 3 documentations (#488)

Reviewed-on: https://gitea.com/gitea/docs/pulls/488
Reviewed-by: silverwind <[email protected]>
This commit is contained in:
Lunny Xiao
2026-08-07 21:02:26 +00:00
parent 7629c02a9a
commit 0c62ec4893
57 changed files with 5215 additions and 37 deletions
+153
View File
@@ -0,0 +1,153 @@
---
sidebar_position: 1
---
# Install from a binary
The runner is a single static binary called `gitea-runner`. It has no dependencies apart from a Docker daemon for containerized jobs.
## Download
- released builds: [dl.gitea.com/gitea-runner](https://dl.gitea.com/gitea-runner/) or the [release page](https://gitea.com/gitea/runner/releases)
- development builds of the `main` branch: [dl.gitea.com/gitea-runner/nightly](https://dl.gitea.com/gitea-runner/nightly/)
Each file is published next to a `.sha256` checksum and an `.xz` compressed variant:
```bash
VERSION=nightly
curl -sSLO "https://dl.gitea.com/gitea-runner/$VERSION/gitea-runner-$VERSION-linux-amd64"
curl -sSLO "https://dl.gitea.com/gitea-runner/$VERSION/gitea-runner-$VERSION-linux-amd64.sha256"
sha256sum -c "gitea-runner-$VERSION-linux-amd64.sha256"
install -m 0755 "gitea-runner-$VERSION-linux-amd64" /usr/local/bin/gitea-runner
```
Check that the binary matches your platform:
```bash
gitea-runner --version
```
## Build from source
Building requires the Go version declared in the repository's `go.mod`:
```bash
git clone https://gitea.com/gitea/runner.git
cd runner
make build
```
## First run
```bash
gitea-runner generate-config > config.yaml # optional, defaults are safe
gitea-runner -c config.yaml register # see "Registering a runner"
gitea-runner -c config.yaml daemon
```
The `daemon` command runs in the foreground. It reads the registration file (`runner.file`, `.runner` by default) relative to its working directory, so keep the working directory stable across restarts.
## Run as a systemd service
Create an unprivileged user, install the binary, and register the runner as that user so the `.runner` file ends up in the service's working directory:
```bash
sudo useradd --system --home-dir /var/lib/gitea-runner --create-home gitea-runner
sudo install -d /etc/gitea-runner
sudo -u gitea-runner gitea-runner generate-config | sudo tee /etc/gitea-runner/config.yaml >/dev/null
cd /var/lib/gitea-runner
sudo -u gitea-runner gitea-runner register -c /etc/gitea-runner/config.yaml
```
Then install the unit as `/etc/systemd/system/gitea-runner.service`:
```ini
[Unit]
Description=Gitea Actions runner
Documentation=https://gitea.com/gitea/runner
After=network-online.target
Wants=network-online.target
# Uncomment when jobs use the local Docker daemon:
# After=docker.service
# Requires=docker.service
[Service]
Type=simple
ExecStart=/usr/local/bin/gitea-runner daemon --config /etc/gitea-runner/config.yaml
WorkingDirectory=/var/lib/gitea-runner
User=gitea-runner
Group=gitea-runner
Restart=on-failure
RestartSec=5s
# Allow running jobs to finish before the runner is stopped. Keep this in sync
# with runner.shutdown_timeout in the config.
TimeoutStopSec=3h
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now gitea-runner
```
If jobs use the host's Docker daemon, the `gitea-runner` user also needs access to the daemon socket. Adding it to the `docker` group grants that access and is [equivalent to root on the host](https://docs.docker.com/engine/security/#docker-daemon-attack-surface).
Environment variables for the process — most importantly [proxy variables](../proxy.md) — belong in `Environment=` lines or a drop-in file, not in the runner config.
## Run as a launchd daemon (macOS)
macOS uses `launchd` instead of systemd. Daemons run as `root` by default; an unprivileged `_gitea-runner` user can be created with `dscl`. Install the following as `/Library/LaunchDaemons/com.gitea.runner.plist` and adjust the paths to your installation:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.gitea.runner</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/gitea-runner</string>
<string>daemon</string>
<string>--config</string>
<string>/etc/gitea-runner/config.yaml</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>WorkingDirectory</key>
<string>/var/lib/gitea-runner</string>
<key>StandardOutPath</key>
<string>/var/lib/gitea-runner/runner.log</string>
<key>StandardErrorPath</key>
<string>/var/lib/gitea-runner/runner.err</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>HOME</key>
<string>/var/lib/gitea-runner</string>
</dict>
<key>UserName</key>
<string>_gitea-runner</string>
</dict>
</plist>
```
```bash
sudo launchctl load /Library/LaunchDaemons/com.gitea.runner.plist
```
On macOS and Windows hosts, jobs usually run with [host labels](../labels.md) and the tools installed on the machine.
## Windows
Install the `windows-amd64` binary and register it as a service with any service wrapper (for example `sc.exe` plus a wrapper such as [WinSW](https://github.com/winsw/winsw), or a scheduled task at boot). The runner itself has no service-installer subcommand.
Keep in mind for Windows hosts:
- `runner.post_task_script` accepts `.exe`, `.bat` and `.cmd` paths; `.ps1` is not supported as the configured path.
- host-mode jobs are terminated as a process tree, so tools that daemonize are not left behind.
+147
View File
@@ -0,0 +1,147 @@
---
sidebar_position: 2
---
# Install with Docker
The official images are published on [Docker Hub](https://hub.docker.com/r/gitea/runner/tags) 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.
| Tag | Base image | Docker daemon | Supervisor | Runs as |
| --- | --- | --- | --- | --- |
| `nightly`, `latest`, `<version>` | `alpine` | none, you provide one | `tini` | `root` |
| `nightly-dind`, `latest-dind` | `docker:dind` | bundled, needs `--privileged` | `s6` | `root` |
| `nightly-dind-rootless`, `latest-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
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:
```bash
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:
```bash
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`](https://gitea.com/gitea/runner/src/branch/main/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](../registration.md#ephemeral-runners) |
| `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](../configuration.md).
Mount the config file when you need one:
```bash
docker run -v $PWD/config.yaml:/config.yaml -e CONFIG_FILE=/config.yaml ...
```
A config file can be generated with the image itself:
```bash
docker run --rm --entrypoint="" docker.io/gitea/runner:nightly gitea-runner generate-config > config.yaml
```
## docker compose
```yaml
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:
```yaml
depends_on:
gitea:
condition: service_healthy
restart: true
```
The rootless Docker-in-Docker variant needs a few extra options:
```yaml
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](../cache.md#dockerized-runners).
More deployment examples live in the [`examples`](https://gitea.com/gitea/runner/src/branch/main/examples) directory of the runner repository.
+49
View File
@@ -0,0 +1,49 @@
---
sidebar_position: 3
---
# Install on Kubernetes
Ready-to-adapt manifests live in [`examples/kubernetes`](https://gitea.com/gitea/runner/src/branch/main/examples/kubernetes) of the runner repository, and a Helm chart is maintained at [gitea/helm-actions](https://gitea.com/gitea/helm-actions).
## Choosing a manifest
| Example | Shape | Docker daemon |
| --- | --- | --- |
| `dind-docker.yaml` | `Deployment` with a native sidecar (`initContainer` with `restartPolicy: Always`, needs Kubernetes 1.29+) | privileged `docker:dind` sidecar, socket shared through an `emptyDir` |
| `statefulset-dind.yaml` | `StatefulSet` with `volumeClaimTemplates` | same as above |
| `rootless-docker.yaml` | `Deployment` with a single container | bundled rootless daemon of the `dind-rootless` image, reached over `tcp://localhost:2376` with TLS |
Prefer the `StatefulSet` variant when you scale past one replica: each pod then gets a stable identity and its own volume, so it keeps its `.runner` registration across restarts and reschedules instead of registering itself again as a new runner.
## Two volumes, two purposes
- `/data` — the runner's working directory, holding the `.runner` registration file and optionally the config file.
- the daemon's data root — `/var/lib/docker` for the `dind` sidecar, `/home/rootless/.local/share/docker` for `dind-rootless`. It holds the images pulled for jobs and is *not* under `/data`. Dropping it still works, but every recreated pod re-pulls all job images.
With the rootless image, both volumes must be writable by UID/GID 1000, which is what `securityContext.fsGroup: 1000` in the example is for.
## Registration token
The examples read the token from a `Secret`:
```yaml
env:
- name: GITEA_INSTANCE_URL
value: http://gitea-http.gitea.svc.cluster.local:3000
- name: GITEA_RUNNER_REGISTRATION_TOKEN
valueFrom:
secretKeyRef:
name: runner-secret
key: token
```
The token stays valid for further registrations until it is reset, but each registration creates another runner entry. For pods that are recreated without their volume, use the instance-wide token configured on the Gitea side, and expect stale runner entries — or start an [ephemeral runner](../registration.md#ephemeral-runners) per job.
## Privileges
Docker-in-Docker needs `securityContext.privileged: true`, which lets a malicious job break out of the container. Weigh that against the alternatives:
- the rootless flavour, which reduces but does not remove the exposure;
- pointing the basic flavour at a daemon outside the cluster;
- keeping such runners on a dedicated node pool and only granting them to trusted repositories.