Configuring a runner

A paired runner reads a saggar.toml: --config <path>, else ~/.config/saggar/saggar.toml ($XDG_CONFIG_HOME honored), else /etc/saggar/saggar.toml. Precedence, increasing: built-in defaults, the config file, SAGGAR_* environment variables, CLI flags. Unknown keys anywhere are refused: a typo must never silently disable the setting it was meant to set.

What you can configure on your runner:

data_dir = "~/.local/share/saggar"

Root for everything the runner keeps on disk: build work trees (work/), warm caches (cache/), cloned or uploaded sources (sources/), job logs (logs/), the artifact upload spool (blobs/) and the scratch root (scratch/). Env: SAGGAR_DATA_DIR; flag --data-dir.

node_id = "<hostname>"

The id the runner pairs and appears under. Default: the machine hostname. Env: SAGGAR_NODE_ID.

[envs]

Environment resolution: fingerprint → OCI image ref.

[envs]
"debian:12" = "debian:12"
"debian:12 + rust" = "rust:1-bookworm"

Entries are optional: each plugin declares its own default image. It is the official one for its toolchain, rolling where the vendor publishes a rolling tag (rust:1, node, python:3, ruby:3, golang:1, swift, all on the fingerprint’s own Debian release; full variants, never slim), or a fixed ref where it does not: uv pins its own distro, dotnet ships latest, composer and elixir stay pinned by hand and bumped when their line retires. For the JVM family the image derives from the project: gradle reads the toolchain level the build files declare, maven the compiler properties (never below 17). A table entry always wins over a plugin’s declaration. Pin images here for the toolchains no plain distro image carries: zig, snap (snapcraft-capable), flatpak (flatpak-builder-capable), rpm (a Fedora/CentOS image), appimage (FUSE + network). A fingerprint for another distro with no entry fails the job; there is no environment to run it in.

[incremental]

The warm caches (warm trees and env snapshots share the budget):

  • ttl_hours = 168: hours an entry may sit unused before eviction.

  • max_total_gb = 20: total size budget; entries evict least-recently-used first.

  • ccache = false: bind a persistent host compiler cache into image builds (opt-in; it only pays off on images that carry ccache).

Warm caches are runner-local. They speed up whatever lands on this machine, and a job may land on a cold one.

[network]

Registry-mirror pinning, deliberately pull-side only: these keys route the runner’s own fetches. Arbitrary egress inside a build is not contained by them.

  • registry_mirror: docker-format base-image pulls go through this pull-through mirror (e.g. https://mirror.internal:5000 fronting docker.io). The podman builder reads a job-scoped registries.conf pinning docker.io to it; the docker daemon builder cannot take a per-invocation registries config and ignores the pin (configure the mirror in daemon.json). Absent: pulls go to the image’s own registry. There is no apt_mirror key: the Debian builds bootstrap their chroot through the pkh library, which exposes no mirror option.

[atlas]

url: the pkgatlas resolve instance; omit it and dependency resolution uses heuristics only. When set, missing-native-dependency evidence resolves through it (exact file→package answers for the image’s distro, release and arch).

[plugins.debian]

prune = false: sweep pkh’s residue after every Debian build. Only when Debian builds never overlap on the machine (the sweep cannot tell a live concurrent build’s chroot from crashed residue); the runner already sweeps once at startup.

[plugins.docker]
  • builder = "podman": daemonless rootless build (works inside an unprivileged container; needs the userns/uidmap/subuid prerequisites). "docker" opts into the host daemon: it needs the socket shared with the runner.

  • cleanup_image = false: remove the job’s saggar/<name>:<job-id> tag from the builder once the image is archived into the store. Off by default: the tag is part of the build’s output surface (a following docker run on the machine uses it). The build cache and base images are always kept.

What a runner ignores

The service-side sections (listen, token, [storage], [projects], [quotas], [retention], [auth] and the rest) mean nothing where builds run. A [storage] section on a runner is ignored with a loud warning: every artifact byte streams through the service under the job’s lease (see The runner protocol), and store credentials do not belong on a build machine.