cargoxx-pkgs runner
Self-hosted Gitea Actions runner that validates package PRs.
Architecture
+----------------+ +-------------------+ +--------------------+
| Gitea instance | <----> | act_runner | -----> | job container |
| (queues jobs) | poll | (this docker- | docker| cargoxx-runner- |
| | | compose service) | spawn | job:latest |
+----------------+ +-------------------+ +--------------------+
compose.ymlruns the officialgitea/act_runner:nightlyimage, which polls Gitea for jobs and spawns one container per workflow run via the host's Docker socket.flake.nixbuilds the job container image — reproducible, declarative, shipsnix,git,curl,jq,tea, and a single-userNIX_CONFIG. This image runs the actual workflow steps; act_runner just orchestrates it.config.yamlmapsruns-on: self-hosted(from.gitea/workflows/*.yml) to the job image.
One-time setup
-
Build + load the job image into the host's Docker daemon:
cd runner nix run --extra-experimental-features 'nix-command flakes' .#load-imageRe-run whenever you bump nixpkgs or change the tool list.
-
Mint a runner registration token in the Gitea UI:
Site Administration → Actions → Runners → Create new Runner. Copy the token. -
Provision the
.envalongsidecompose.yml:GITEA_INSTANCE_URL=https://git.amadey.xyz GITEA_RUNNER_REGISTRATION_TOKEN=<paste here> GITEA_RUNNER_NAME=cargoxx-pkgs-runner GITEA_RUNNER_LABELS=self-hosted -
Generate the binary-cache signing key + cache directory. The workflow's "push to binary cache" step writes here; nginx (or anything you point at it) serves it back over HTTPS to consumers.
mkdir -p cache/store nix-store --generate-binary-cache-key \ cache.cargoxx.<your-domain> \ cache/cache.sec cache/cache.pub chmod 600 cache/cache.secThe
cache/directory is gitignored. Both keys live alongsidecompose.yml; the named volume binds use${PWD}/cache/.... -
Pick the Caddy ports.
compose.ymlruns Caddy alongside the runner to HTTPS-front the cache. Because the router does PAT, the internal ports Caddy listens on must equal whatever 80/443 are forwarded to. Add to.env:CADDY_HTTP_PORT=8080 CADDY_HTTPS_PORT=8443Both compose.yml and the Caddyfile pick those up. The Caddyfile already targets
cache.cargoxx.amadey.xyzand the e-mailvorontsov@amadey.xyz; edit if you're deploying somewhere else.ACME provisioning works as long as the router forwards 80 → CADDY_HTTP_PORT and 443 → CADDY_HTTPS_PORT, so Let's Encrypt's HTTP-01 challenge reaches Caddy.
Consumers' substituter config (
substituters = https://cache.<domain>,trusted-public-keys = <cache.pub>) is baked into cargoxx's own wrapper (cargoxx/flake.nix:cargoxxNixConfig), so any installedcargoxxbinary picks them up — no per-user setup needed. -
Start the runner:
docker compose up -d docker compose logs -f runnerFirst boot registers the runner with Gitea; subsequent boots reuse the persisted token in
./data/.runner.
Updating
- Change a workflow step's tools → edit
flake.nix, rerunnix run .#load-image. - Bump nixpkgs →
nix flake updatein this dir, rebuild the image. - act_runner itself updates by
docker compose pull && docker compose up -d.
Why not host mode?
Host mode (act_runner running workflows directly on the host) would save the Docker indirection but tightly couples the runner host to the toolchain. Docker mode lets us treat the job image as a deployable artifact — same image on every runner, no drift.