Podman-based Gitea Actions runners
Run two independent Gitea Actions runners on one host as rootless Podman containers managed by systemd (Quadlet) — one per project — instead of two VMs. Uses the host executor: workflow steps run directly inside a purpose-built runner image that already has the Rust toolchain and all build dependencies baked in, so CI runs skip the per-run install cost and no Docker/Podman socket is needed.
Files
| File | Purpose |
|---|---|
Containerfile |
Runner image: Debian + build deps + clang + Rust + Node + reuse + act_runner. |
entrypoint.sh |
Registers on first boot (if needed), then runs the daemon. |
config.yaml |
act_runner config template (seeded into each runner's volume). |
trx-rs-runner.container |
Quadlet unit for the trx-rs runner. |
project2-runner.container |
Quadlet unit for the second project's runner. |
Prerequisites (once per host)
Rootless Podman with cgroups v2 (default on modern distros). As the unprivileged user that will own the runners:
# Survive logout / start on boot without an interactive session.
loginctl enable-linger "$USER"
No podman.socket is required for the host executor.
1. Build the image
cd container
podman build -t trx-rs-ci:latest .
2. Get a registration token
For each repo: Settings → Actions → Runners → Create new Runner and copy the token. (Org- or instance-level tokens work too if you prefer wider scope.)
3. Install and start the runners
mkdir -p ~/.config/containers/systemd
cp trx-rs-runner.container project2-runner.container ~/.config/containers/systemd/
# Paste each repo's token for the FIRST boot only:
# Environment=GITEA_RUNNER_REGISTRATION_TOKEN=xxxx…
$EDITOR ~/.config/containers/systemd/trx-rs-runner.container
$EDITOR ~/.config/containers/systemd/project2-runner.container
systemctl --user daemon-reload
systemctl --user start trx-rs-runner
systemctl --user start project2-runner
systemctl --user status trx-rs-runner
podman logs -f gitea-runner-trx-rs
Once each runner shows online in the repo's runner list, blank out the
GITEA_RUNNER_REGISTRATION_TOKEN line again (the registration is persisted in
the …-data volume) and systemctl --user daemon-reload.
Required workflow change: the reuse job
The host executor runs steps directly in the container and therefore cannot
run Docker-based actions. The current reuse job uses fsfe/reuse-action@v5,
which is a Docker action. reuse is baked into the image, so replace that job
with a plain command:
reuse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: REUSE compliance
run: reuse lint
The lint and test jobs need no changes: their sudo apt-get … and rustup
steps still run, but become fast no-ops because the image already has those
packages and the toolchain. (sudo is included in the image for exactly this
reason.)
If you would rather keep Docker-based actions and per-run images, use the Docker executor instead: drop the
:hostsuffix from the label inconfig.yaml, enablesystemctl --user --now enable podman.socket, mount it into the container, and setcontainer.docker_hostto the socket path. That trades the baked-in speed for stronger per-job isolation.
Tuning
capacity(inconfig.yaml) — concurrent jobs per runner. Rust builds are heavy; 1–2 is sensible when two runners share a host.PodmanArgs=--cpus/--memory(in each.container) — hard resource caps so one project cannot starve the other.- SELinux — the
:Zvolume flag is already set; keep it if SELinux is enforcing.
Committing these files
If you add this directory to a REUSE-checked repo, register the markdown in
REUSE.toml (the other files carry inline SPDX headers):
[[annotations]]
path = ["container/**"]
SPDX-FileCopyrightText = "2026 Stan Grams <sjg@haxx.space>"
SPDX-License-Identifier = "GPL-2.0-or-later"