Central runner on ana-docker (gitea is local; existing fleet tooling already SSHes from there). Playbook is parameterized so future site-local runners (nh3-docker, esh-docker-vm) drop in via --var overrides instead of copy-paste. Includes a workflow template for vh/task-board that calls the existing deploy-task-board.yaml playbook — keeps the playbook as the single source of truth for "how task-board is deployed", manual or automated. Labels embed `:docker://node:20-bookworm-slim` schema; without it, act_runner v0.6+ silently falls back to host-mode and runs job steps inside the Alpine runner container (no apt/python/node), breaking any real workflow. node:20-bookworm-slim is small + has git + node so actions/checkout works out of the box.
5.4 KiB
gitea-runner
Self-hosted Gitea Actions
runner. Polls gitea.phasefinal.com for jobs from any repo that has a
.gitea/workflows/ directory and runs them in ephemeral docker
containers on this host.
Server: ana-docker (single central runner — see "Topology" below
for when to add more)
Image: gitea/act_runner:latest
Outbound only — no host port published; the runner connects out to
gitea, gitea never connects in.
Topology
We run one central runner on ana-docker. Reasoning:
- gitea is on ana-docker, so runner→API is local
- existing fleet tooling (
elway,sync-stacks,deploy-stack,refresh-server-info) already SSHes from one origin to all hosts; the runner inherits that pattern - single point to manage SSH keys, secrets, and runner upgrades
The deploy playbook (playbooks/deploy-gitea-runner.yaml) is
parameterized by host / runner-name / labels, so spinning up
nh3-docker-runner or an ESH runner later is a one-line elway
invocation — not a copy-pasted playbook.
When to add a site-local runner:
- Cross-site SSH from ana-docker to that site has become unreliable
- A workflow needs LAN access to something only reachable from inside that site's network segment
- You want failure isolation (NH3 can deploy itself when Anaheim is down)
Until one of those bites, one runner is enough.
Prereqs
Before running the deploy playbook:
-
Verify Gitea Actions is enabled. In gitea 1.21+ Actions ships on by default, but check
/-/admin/actionsresolves. If not, addGITEA__actions__ENABLED=trueto the gitea stack env and bounce. -
Generate a registration token. Pick the scope:
Scope URL Use when Global (admin) https://gitea.phasefinal.com/-/admin/actions/runnersrunner serves any repo on the instance (recommended for the central PFI runner) Org/user https://gitea.phasefinal.com/<owner>/-/actions/runnersrunner serves all repos under one owner Repo https://gitea.phasefinal.com/<owner>/<repo>/settings/actions/runnersrunner serves one repo Register-as-admin is right for our use case: one runner, fleet-wide.
-
Create an SSH deploy key for the runner that lets it execute the elway playbooks against fleet hosts. The key lives only on ana-docker (passed in as a workflow secret per repo, or mounted into the runner via volume — see "Wiring deploys" below).
-
(Optional) Create a Gitea PAT with
read:repositoryscope onvh/esh-pfi-infrastructure. Workflows need to check out the management repo to get at the playbooks; the auto-injectedGITHUB_TOKENonly works for the triggering repo.
Deploy
# Edit .env first if not using defaults — at minimum paste the registration token
$EDITOR stacks/gitea-runner/.env.example # template
# First-time deploy
scripts/elway ana-docker --playbook playbooks/deploy-gitea-runner.yaml
# Site-local runner later (NH3 or ESH)
scripts/elway nh3-docker --playbook playbooks/deploy-gitea-runner.yaml \
--var runner_name=nh3-docker-runner --var runner_labels=pfi-fleet,nh3-docker
The playbook seeds .env from .env.example only if absent; for the
first run, copy .env.example to /opt/docker/compose/gitea-runner/.env
on the host and paste the registration token in before running, OR
let the playbook seed it and edit on the host before the
docker compose up -d step (it's idempotent — second run will pick up
the edited token).
After successful registration, the token is consumed (it's one-time
use). You can clear GITEA_RUNNER_REGISTRATION_TOKEN from .env;
the runner reads its permanent credentials from ${DATA_DIR}/.runner
on subsequent starts.
Wiring deploys
A workflow that runs on the central runner needs three things:
runs-on:matching a runner label —pfi-fleet(cross-fleet) orana-docker(pin to that host).- An SSH key to reach the deploy target. Stored as a repo or
org-level Actions secret named e.g.
DEPLOY_SSH_KEY. The corresponding public key must be in~lkraven/.ssh/authorized_keyson every host the workflow targets. - A token to clone
vh/esh-pfi-infrastructureif the workflow wants to invoke an elway playbook from this repo. Stored asMGMT_REPO_TOKEN(Gitea PAT,read:repositoryscope).
See stacks/task-board/gitea-workflow-deploy.yaml.example for a
complete deploy workflow that consumes all three.
Path layout (on ana-docker)
| Host path | Container path | Purpose | Restic? |
|---|---|---|---|
/opt/docker/compose/gitea-runner/ |
— | compose.yaml + .env | included (via /opt/docker) |
/opt/docker/conf/gitea-runner/data/ |
/data |
.runner creds, cache, job workspaces |
excluded (regenerable; nothing irreplaceable) |
Operations
# Tail runner logs
ssh ana-docker docker logs -f gitea-runner
# List currently registered runners (admin)
# https://gitea.phasefinal.com/-/admin/actions/runners
# Re-register (lost the .runner file? regenerate token, then:)
ssh ana-docker docker compose -f /opt/docker/compose/gitea-runner/compose.yaml down
ssh ana-docker rm /opt/docker/conf/gitea-runner/data/.runner
# paste new GITEA_RUNNER_REGISTRATION_TOKEN into .env
scripts/elway ana-docker --playbook playbooks/deploy-gitea-runner.yaml
# Pin to a specific act_runner version
# Edit RUNNER_IMAGE in /opt/docker/compose/gitea-runner/.env, then:
scripts/elway ana-docker --playbook playbooks/deploy-gitea-runner.yaml