Files
esh-pfi-infrastructure/stacks/gitea-runner
vh f014d5534a gitea-runner: stack + playbook for self-hosted Actions
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.
2026-04-29 18:12:36 -07:00
..

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:

  1. Verify Gitea Actions is enabled. In gitea 1.21+ Actions ships on by default, but check /-/admin/actions resolves. If not, add GITEA__actions__ENABLED=true to the gitea stack env and bounce.

  2. Generate a registration token. Pick the scope:

    Scope URL Use when
    Global (admin) https://gitea.phasefinal.com/-/admin/actions/runners runner serves any repo on the instance (recommended for the central PFI runner)
    Org/user https://gitea.phasefinal.com/<owner>/-/actions/runners runner serves all repos under one owner
    Repo https://gitea.phasefinal.com/<owner>/<repo>/settings/actions/runners runner serves one repo

    Register-as-admin is right for our use case: one runner, fleet-wide.

  3. 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).

  4. (Optional) Create a Gitea PAT with read:repository scope on vh/esh-pfi-infrastructure. Workflows need to check out the management repo to get at the playbooks; the auto-injected GITHUB_TOKEN only 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:

  1. runs-on: matching a runner label — pfi-fleet (cross-fleet) or ana-docker (pin to that host).
  2. 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_keys on every host the workflow targets.
  3. A token to clone vh/esh-pfi-infrastructure if the workflow wants to invoke an elway playbook from this repo. Stored as MGMT_REPO_TOKEN (Gitea PAT, read:repository scope).

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