Skip to content

Forgejo Runner

The runner that executes .forgejo/workflows/ — the standby copy of the GitHub Actions pipelines. It lives on the same ubuntu-vm host as github-runner, alongside it rather than instead of it, so either forge can build at any time.

github-runner runs jobs directly inside its own container. Forgejo Actions instead starts a fresh container per job from an image named in the runner’s label map, so the toolchain lives in a separate image — builder/Dockerfile, published to Harbor as toolsera/forgejo-builder — and the runner container only brokers work.

That image pins the same fpm, yq and bws versions as github-runner, so a .deb built here matches one built on GitHub Actions.

github-runnerforgejo-runner
Job executionin the runner containerone container per job
Toolchainbaked into the runner imagetoolsera/forgejo-builder
Docker accesshost sockethost socket, passed through to job containers
CA trust~/.step/certs/root_ca.crt bind mounthost CA bundle bind mount

Everything below runs on the ubuntu-vm host, from the repo checkout.

Chicken-and-egg: the image that runs the workflows cannot be built by those workflows the first time. Build it by hand once —

Terminal window
cd docker-compose-apps/ubuntu-vm/apps/forgejo-runner/builder
docker build -t harbor.toolsera.lan/toolsera/forgejo-builder:latest .
docker push harbor.toolsera.lan/toolsera/forgejo-builder:latest

— and after that let CI own it, by pushing a tag:

Terminal window
git tag image/forgejo-builder-0.1.0 && git push origin image/forgejo-builder-0.1.0

Both publish-docker-image.yml workflows carry forgejo-builder in their build context lookup tables, so the tag builds it from either forge.

Forgejo 15 does not issue legacy registration tokens, and forgejo-runner register is deprecated — it fails here with invalid_argument: runner registration token not found. The runner is declared in config.yml instead, per the Forgejo registration docs.

In the Forgejo UI, open the Runners page for the scope you want and use Create new runner:

ScopePathRuns workflows from
Instance/admin/actions/runnersevery repository
Organisation/org/{org}/settings/actions/runnersthat org
User/user/settings/actions/runnersthat user’s repos
Repository/{owner}/{repo}/settings/actions/runnersone repository

It shows a UUID and a token. They go to two different places:

  • the UUID is an identifier, not a secret — it lives in config.yml under server.connections.toolsera.uuid, committed to git;
  • the token is confidential and never enters the repo. Write it to data/token.txt, which config.yml reads through token_url. data/ is gitignored.
Terminal window
cd /root/infrastructure-toolkit/docker-compose-apps/ubuntu-vm/apps/forgejo-runner
mkdir -p data
# No trailing newline, no shell history: the file must hold the token and nothing else.
read -rs TOKEN # paste the token, press Enter
printf '%s' "$TOKEN" > data/token.txt
chmod 600 data/token.txt
unset TOKEN
wc -c data/token.txt # sanity check: non-zero, and no stray newline

Then put the UUID into config.yml in the repo, commit, push, and git pull on the host — not by editing the file in place, which the next pull would conflict with.

There is no .runner file in this flow; a declared connection does not register, so there is no registration state to lose.

Terminal window
cd docker-compose-apps/ubuntu-vm
docker compose up -d forgejo-runner
docker compose logs -f forgejo-runner

A healthy runner logs runner: ... , with ... labels and then polls quietly. It should show as Idle under the repo’s Actions → Runners page.

config.yml is mounted read-only and is the only place to change runner behaviour. The two settings worth understanding:

  • runner.labels maps a runs-on: value to the image the job runs in. All three workflows use runs-on: deb-builder; homelab and ubuntu-latest are mapped to the same image so a job pasted over from .github/workflows/ still finds a runner.
  • container.options bind-mounts the host CA bundle over each job container’s. The host already trusts the Step CA (the step_ca_client Ansible role installs it), and without this a job cannot even clone from forgejo.toolsera.lan. Node keeps its own trust store and ignores the bundle, which is why every workflow sets NODE_EXTRA_CA_CERTS.

container.docker_host: "" leaves the host Docker socket mounted into job containers — that is what lets publish-docker-image.yml run buildx — so a job on this runner has root-equivalent access to the host daemon. Only run workflows from branches and tags you trust.

Failures live in one place, by symptom: .forgejo/TROUBLESHOOTING.md. It covers the ones that land here — jobs stuck in “Waiting”, x509: certificate signed by unknown authority, registration token not found, docker: command not found inside a job — along with the forge-side problems that look like runner problems.