ci: the runner is docker-mode, and a guide for setting one up

`runs-on: ubuntu-latest` stays what it is everywhere else — a disposable
container — and `docker-compose.ci.yml` is now what makes that true: the label
maps to `catthehacker/ubuntu:act-24.04`, job containers share the runner's
network so the cache server is reachable at all, and the host's docker socket is
passed through (`docker_host: automount`) so the image workflow can build.

The failure this is replacing is worth naming, because the symptom does not
point at it: with a **host**-mode runner answering the same label, every job runs
directly on the server as the runner's own user, so the workflows' `apt-get
install` steps fail on permissions — and had they succeeded, CI would have been
installing packages on a host. A workflow cannot tell which kind of runner it
got; only the label map can say.

`docs/gitea-runner.md` is the separate guide: why docker mode, the prerequisites
including Gitea's two `app.ini` settings, the install, how to remove a host-mode
runner (and what to check before you do, since a runner is shared), how to
verify a job really is in a container, why labels cannot be changed without
re-registering, and a table of the failures with their causes.

Also `workflow_dispatch` on the test workflow, so a fresh runner can be proved
from the UI without inventing a commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
timka123andClaude Opus 5 committed 2026-10-22 18:35:20 +03:00
1 parent 38f346883c
commit 8415c8995b
6 files changed
+299 -59

No files matched your search

+58 -34
View File
@@ -1,48 +1,72 @@
# The Gitea Actions runner, for a single-machine Gitea.
# The Gitea Actions runner, in Docker.
#
# Nothing in .gitea/workflows runs until a runner is registered, and this is the
# whole of it. Two settings do the work:
# This is the runner that executes everything in `.gitea/workflows`. It claims
# the label **`ubuntu-latest`** and maps it to a container image, which is what
# the workflows expect: GitHub's own name, GitHub's own semantics — the job runs
# in a disposable container, not on the machine.
#
# * the **label map** decides what `runs-on: ubuntu-latest` means. It is pinned
# to catthehacker/ubuntu:act-24.04 because the workflows need two things from
# the image: a node (Gitea's javascript actions — checkout, cache,
# upload-artifact — run *inside* the job container) and Python 3.12, which
# the backend's `requires-python` insists on. Ubuntu 24.04 ships 3.12.
# * the **docker socket**, mounted into job containers so the `docker` workflow
# can build and push an image. That is a real privilege handed to CI; it is
# also the normal arrangement when Gitea and its runner are the same box as
# the deployment.
# It replaced a *host*-mode runner on the same box (`ubuntu-latest:host`), where
# every job ran directly on the server as the `act_runner` user. That is why the
# workflows' `apt-get install` steps failed: nothing could be installed, and had
# it succeeded it would have been installing packages on the host.
#
# Setting it up:
# Setting it up, and what each piece is for, is in
# [`docs/gitea-runner.md`](docs/gitea-runner.md). The short version:
#
# 1. Gitea → Settings → Actions → Runners → "Create new runner", copy the token.
# 2. GITEA_RUNNER_REGISTRATION_TOKEN=<token> docker compose -f docker-compose.ci.yml up -d
# 3. Repository → Settings → Actions → Secrets, add:
# REGISTRY_USER, REGISTRY_TOKEN — a Gitea user and token with
# package:write, for the registry
# KADENCE_KEYSTORE_BASE64 — base64 -w0 of your keystore
# KADENCE_KEYSTORE_PASSWORD, KADENCE_KEY_ALIAS, KADENCE_KEY_PASSWORD
# Without the keystore secrets the APK job still runs and produces an
# unsigned-for-release (debug-signed) artifact rather than failing.
# mkdir -p /opt/gitea-runner && cd /opt/gitea-runner
# # copy this file as docker-compose.yml and deploy/act-runner.yaml as config.yaml
# printf 'GITEA_RUNNER_REGISTRATION_TOKEN=%s\n' "<token from Gitea>" > .env
# chmod 600 .env
# docker compose up -d
#
# Gitea itself needs `[actions] ENABLED = true` in app.ini, and
# `DEFAULT_ACTIONS_URL = github` so `actions/checkout@v4` can be resolved.
# The registration token is needed **once**: after the first start the runner has
# its own permanent token in `./data/.runner` and the one in `.env` is dead.
services:
runner:
image: gitea/act_runner:0.2.11
image: gitea/act_runner:0.3.1
container_name: gitea-runner
restart: unless-stopped
environment:
GITEA_INSTANCE_URL: ${GITEA_INSTANCE_URL:-https://git.timka.su}
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN:?set this to the token from Gitea}
GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME:-kadence-runner}
GITEA_RUNNER_LABELS: ubuntu-latest:docker://catthehacker/ubuntu:act-24.04
# Mounted into every job container, which is what lets the docker workflow
# build an image. `valid_volumes` has to allow it explicitly.
# Only read at first registration; harmless (and ignored) afterwards.
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN:-}
GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME:-dockerized}
# The label map: what `runs-on` may ask for, and what it gets.
#
# `act-24.04` and not something slimmer because the jobs need two things
# from the image: a **node**, since Gitea's javascript actions (checkout,
# cache, upload-artifact) run *inside* the job container, and **Python
# 3.12**, which the backend's `requires-python` insists on and Ubuntu
# 24.04 ships. A plain `ubuntu:24.04` has neither and every job would
# fail at the first `uses:`.
#
# Labels are baked into `./data/.runner` at registration — changing this
# line later does nothing on its own. See the guide.
GITEA_RUNNER_LABELS: >-
ubuntu-latest:docker://catthehacker/ubuntu:act-24.04,
ubuntu-24.04:docker://catthehacker/ubuntu:act-24.04,
ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04
CONFIG_FILE: /config/config.yaml
volumes:
- runner-data:/data
- ./deploy/act-runner.yaml:/config/config.yaml:ro
- ./config.yaml:/config/config.yaml:ro
# The registration lives here. Delete `./data/.runner` and the runner
# re-registers as a *new* one, leaving a stale entry in Gitea.
- ./data:/data
# The host's docker daemon, so a job can build and push an image. This is
# a real privilege — root on the host, effectively — and the reason the
# runner should not be registered at instance level on a box you do not
# want CI to own. `config.yaml` passes it through to job containers.
- /var/run/docker.sock:/var/run/docker.sock
volumes:
runner-data:
networks:
- runner
networks:
# Job containers join this same network (`container.network` in config.yaml),
# which is what makes the runner's built-in cache server reachable from them:
# it advertises its own address, and on a separate network that address would
# route nowhere. It is also why nothing here needs a published port.
runner:
name: gitea-runner