`docker_host: automount` is a later act_runner's spelling of the default, and 0.3.1 does not know the word: it exits with `unable to parse docker host automount` and restart-loops — registered, visible in Gitea, never online, and the logs say nothing about the config file. On 0.3.x the empty value is the one that finds a daemon *and* passes it to the job containers, which is what `docker.yml` needs; `"-"` is the value that withholds it. Both spellings are now in the guide's failure table, because the symptom points at the daemon rather than at the one word that caused it. Verified in the job image on the runner host: Ubuntu 24.04 as root, node 24, Python 3.12.3, npm, curl and unzip (the SDK script's dependencies), apt-get update/install, `python3 -m venv` plus pip, `docker version` against the host's 27.5.1 through the mounted socket, and openjdk-21-jdk-headless landing exactly at the `JAVA_HOME` the Android jobs assume. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.7 KiB
A dockerized Gitea Actions runner
Everything in .gitea/workflows needs a runner, and it has to be a docker-mode
one. This is how to set one up, what each setting is for, and what goes wrong if
it is set up the other way.
- Why docker mode and not host mode
- Prerequisites
- Install
- Removing a host-mode runner
- Verifying it
- Changing the labels later
- What can go wrong
Why docker mode and not host mode
A Gitea runner advertises labels, and each label says where a job runs:
| Label | Means |
|---|---|
ubuntu-latest:docker://catthehacker/ubuntu:act-24.04 |
run the job in a fresh container from that image |
ubuntu-latest:host |
run the job directly on the machine, as the runner's own user |
A workflow only says runs-on: ubuntu-latest. It cannot tell which of those it
got, and the two behave nothing alike:
- Host mode has no job isolation and no job toolchain.
apt-get installeither fails (the runner user is not root — the common case) or succeeds, and then you have CI installing packages on a server.actions/checkoutclones into a temp directory that persists between runs, so one job's leftovers are the next job's surprise. Every workflow written against GitHub's runners assumes a clean container and does not get one. - Docker mode is what
ubuntu-latestmeans everywhere else. A disposable container per job, the tools the image ships, and nothing left behind.
Host mode is for the narrow case where a job must touch the machine — a deploy script using that host's ssh keys, say. Everything else wants docker mode.
One label, one meaning. If two runners both claim ubuntu-latest, jobs land on
whichever answers first, and a workflow that passes on one fails on the other for
reasons nobody can see from the logs. Pick one, or give the special-purpose runner
a label of its own (deploy:host) and leave ubuntu-latest to the docker one.
Prerequisites
-
Docker and the compose plugin on the host (
docker --version,docker compose version). -
Gitea with Actions on. In
app.ini:[actions] ENABLED = true ; so `uses: actions/checkout@v4` can be resolved from GitHub DEFAULT_ACTIONS_URL = github -
Disk. The job image is ~1–2 GB; the Android SDK cache another ~3 GB, and gradle caches grow. Budget 20 GB and it will not surprise you.
-
A registration token, from whichever scope the runner should serve:
Scope Where Serves Repository repo → Settings → Actions → Runners → Create new runner that repo only Organisation org → Settings → Actions → Runners every repo in the org Instance Site Administration → Actions → Runners everything on the instance Prefer the narrowest scope that covers what you need — the runner's docker socket is effectively root on its host, and an instance-level runner hands that to every repository on the server.
Install
Two files, both in this repo: docker-compose.ci.yml (the service) and
deploy/act-runner.yaml (act_runner's own config). On the runner host:
mkdir -p /opt/gitea-runner && cd /opt/gitea-runner
# From a checkout, or scp them across:
cp /path/to/kadence/docker-compose.ci.yml docker-compose.yml
cp /path/to/kadence/deploy/act-runner.yaml config.yaml
# The token is needed once, at registration. Keep it out of the compose file.
printf 'GITEA_INSTANCE_URL=https://git.timka.su\n' > .env
printf 'GITEA_RUNNER_REGISTRATION_TOKEN=%s\n' "<paste the token>" >> .env
printf 'GITEA_RUNNER_NAME=%s\n' "$(hostname)-docker" >> .env
chmod 600 .env
docker compose up -d
docker compose logs -f # expect: "runner registered successfully"
That is it. The runner appears in Gitea under the scope you took the token from, with its labels listed beside it.
What the pieces do
-
GITEA_RUNNER_LABELSis the label map, and it is read only at first registration — see Changing the labels later. -
./dataholds.runner: the runner's identity and its permanent token. Back it up or do not delete it; removing it makes the runner register again as a new one and leaves a dead entry behind in Gitea. -
/var/run/docker.sockis mounted so a job can build images. act_runner passes it into the job container too — on 0.3.x that is what an emptydocker_hostinconfig.yamlmeans, not "disabled". Treat the result as what it is: any workflow on this runner can do anything to this host's docker.Watch the spelling here.
docker_host: "-"finds a daemon for the runner but does not give it to jobs, anddocker_host: automount— the same default spelled out, from a later act_runner — makes 0.3.1 exit withunable to parse docker host automountand restart-loop, registered but never online. -
The shared
gitea-runnernetwork. Job containers join the runner's own network (container.network), because act_runner's built-in cache server — the thing that makesactions/cache@v4work — advertises its own address, and from the default bridge that address routes nowhere. Symptom if you get it wrong: cache steps hang, then time out. -
catthehacker/ubuntu:act-24.04rather thanubuntu:24.04, because Gitea's javascript actions (checkout,cache,upload-artifact) execute inside the job container and need anodethere. This image also ships Python 3.12, which Kadence's backend requires.
Removing a host-mode runner
If the box already has one (the usual sign: jobs fail at apt-get with permission
errors, or succeed and change the host):
systemctl disable --now act_runner.service
mv /var/lib/act_runner/.runner /var/lib/act_runner/.runner.disabled # reversible
rm -f /etc/systemd/system/act_runner.service && systemctl daemon-reload
rm -f /usr/local/bin/act_runner
Then delete its (now offline) entry in Gitea, or it sits there looking available.
Check what else used it first. A runner is shared: look at its recent tasks
(journalctl -u act_runner | grep "task .* repo is") and at the other repos'
workflows. Jobs that ran on the host because they needed the host — deploying
with that machine's ssh keys, writing to a host path — will not work in a
container as they are. The docker socket is mounted, so docker compose steps
generally still do.
Verifying it
docker compose logs --tail 20 # "runner registered" / "daemon started"
In Gitea: the runner is listed, online, with its labels. Then run something — a
push, or the Run workflow button if the workflow has workflow_dispatch. The
first job pulls the image, so give it a few minutes; later ones start in seconds.
Job logs should show the job running as root inside a container:
whoami -> root
cat /etc/os-release -> Ubuntu 24.04
If it says your runner's user and the host's distribution, the job landed on a host-mode runner and the label map is wrong.
Changing the labels later
Labels live in ./data/.runner after registration; the environment variable is
only read when that file does not exist. To change them, re-register:
cd /opt/gitea-runner
docker compose down
rm data/.runner # the old registration goes stale in Gitea
# get a fresh token, put it in .env, adjust GITEA_RUNNER_LABELS
docker compose up -d
Then delete the stale runner in Gitea. (Editing .runner's labels array by hand
works too, but Gitea keeps its own copy, so the UI and the runner can disagree —
re-registering is the honest route.)
What can go wrong
| Symptom | Cause |
|---|---|
| Jobs stay queued forever | No runner claims that runs-on label, or the runner is registered at a scope that does not include this repo |
Cannot find: node in PATH |
A job image without node — use a catthehacker/* image, not bare ubuntu |
apt-get: Permission denied |
The job is on a host-mode runner |
uses: actions/checkout@v4 fails to resolve |
DEFAULT_ACTIONS_URL is not github |
| Cache steps hang | Job containers are not on the runner's network — see container.network |
docker: command not found in a job |
That image has no docker CLI; use the catthehacker one, which does |
Cannot connect to the Docker daemon in a job |
The socket is not mounted into the runner, or docker_host is "-" (found for the runner, withheld from jobs) |
unable to parse docker host …, daemon restart-loops |
docker_host is set to a word this act_runner does not know — automount on 0.3.x. Leave it empty |
| Everything is slow on the first run | It is pulling the job image. It is cached afterwards |