dispatch.toml(5)
NAME
dispatch.toml - operator policy for the dispatcher build host
SYNOPSIS
/etc/dispatcher/dispatch.toml
DESCRIPTION
A build host has a handful of decisions that belong to whoever runs it, not to the software. How many builds run at once. How long a failed build tree is kept around. How often images get recomposed. Whether the web UI is reachable from the LAN. Those all live in this one file.
It's read by the producer-side stack: the dispatch daemon, its subprocess workers, and dispatch-web(1). It's TOML, owned by root, group dispatch, mode
- The daemon reads it and never writes it, and it refuses to start without
it.
Where does it come from?
tmpfiles seeds it from /usr/share/factory/etc/dispatcher/dispatch.toml when it's missing, and never again. Your edits survive every upgrade. If you want to start over, delete the file and run systemd-tmpfiles --create.
The factory file shows every key at its built-in default, commented out. Uncomment a key to tune it. Or uncomment it as-is, to pin today's value against a future change of the default.
When do changes apply?
When the service that reads them restarts. That's dispatcher.service for every section but [web], and dispatch-web.service for [web].
What isn't here?
Three things, on purpose:
- The topology: registries, stage repos, and which repo is the scratch floor. The layout API rewrites that at runtime, so it lives in
/var/lib/dispatcher/topology.toml, which the daemon owns. - Key material. Signing seeds and the auth verify key are files under
/var/lib/dispatcher/keys/, written bydispatch init. Installing or revoking a key is a filesystem act, never an edit here. No secret ever appears in this file; a key that needs one names a file that holds it. - Paths. The deployment layout fixes those.
TOP-LEVEL KEYS
worker_mode="subprocess"|"noop"- How a build job runs.
subprocess, the default, runs each build in a supervised worker process.noopruns nothing at all; it's for testing the daemon itself. worker_run_pipeline= bool- Whether a worker actually fetches, builds, checks reproducibility, signs and publishes. Default
true. Withfalse, every build job fails without building anything. Compose jobs aren't affected. seccomp_deny_action="fatal"|"softgap"|"errno"What a build sandbox does with a syscall its seccomp allowlist doesn't name.
fatal, the built-in default: kill the build with SIGSYS and a coredump.softgap: the dangerous deny set stays fatal, but the unclassified tail returns ENOSYS. A build tool reaching for a harmless syscall nobody listed yet degrades, instead of dying.errno: every blocked syscall returns ENOSYS.
The factory file sets
softgap.fatalis good for hunting allowlist gaps, and noisy everywhere else. A recipe's[build] seccompoverrides this per package. Any other spelling is a parse error, not a quiet default.max_parallel_builds= N- Worker processes running at once, across all runs. Default: half the available parallelism, at least 1. The factory file leaves it unset on purpose, so it scales with the machine. Set it only to cap build load below that.
interrupt_retry_cap= N- How many times a job interrupted by a daemon death (crash, SIGKILL, reboot) is resumed before it's failed. Default 3. Without a cap, a job that kills the daemon on every boot would wedge it in a restart loop.
license_reference_dir= "DIR"- A directory of SPDX reference texts (id
.txt) that extends the bundled license-template corpus during license discovery. Unset by default, which means the bundled corpus only. Set but unreadable or empty fails the build.
THE [gc] TABLE
Build trees are big, somewhere between 250 MB and 1 GB each, and a build host makes a lot of them. This table reclaims the per-job trees under /var/cache/dispatcher/work/, and the logs.
A job that succeeded has its tree reaped as soon as it finishes. A failed one is kept, as long as it's both among the newest max_kept_failures and younger than work_retention_days. That's what you go digging through when a build breaks.
enabled= bool- Default
true.falsestops the eager reap and the sweep.dispatch gcstill works. work_retention_days= N- Default 7.
max_kept_failures= N- Default 20.
max_kept_scratch= N- Succeeded designer closure and scratch trees kept for inspection, newest first. Default 20.
log_retention_days= N- Default 5. It doesn't depend on how long the work tree lives.
sweep_interval_secs= N- Default 3600.
min_free_bytes= N- A disk-pressure floor for the filesystem holding the work trees. Below it, the sweep reaps the oldest kept failures ahead of the windows above. A burst of crashing builds fills a disk long before a 7-day window trips. Default 53687091200 (50 GiB); 0 disables it.
pressure_check_interval_secs= N- How often free space is checked against that floor. It's one statvfs, so it runs far more often than the sweep. Default 60.
THE [converge] TABLE
Convergence is the daemon driving each registry toward its stage repo. It builds recipes that are missing at their current N-V-R, whose build dependencies are present. When this is disabled, dispatch-cli converge still works as a one-shot.
enabled= bool- Default
true. interval_secs= N- Default 120.
max_in_flight= N- Convergence builds in flight at once. Designer scratch builds and composition builds do not count against it. Default 4.
arch= "ARCH"- The architecture the built-state check matches. Default
x86_64. retry_base_secs= N- How long before a same-input failure is retried. It doubles with each consecutive failure. Default 3600.
max_auto_retries= N- Retries after the first attempt. After that the recipe is suppressed, until its inputs change or an operator rebuilds it. Self-healing shouldn't turn into retrying forever. Default 3.
THE [compose] TABLE
The composition drift sweep, how many images are kept, and what an image build publishes next to its artifact. A composition descriptor says what to compose. How often this machine is willing to do it is a property of the machine, so it goes here, not in a descriptor somebody keeps in git.
enabled= bool- The automatic sweep. Default
true. A manual compose request works either way. interval_secs= N- Default 300. The sweep also wakes up when a build completes, since a publish may have moved a closure. So this is the limit on how stale a drift check can get, not the only time one happens.
max_in_flight= N- Default 1. A disk composition writes gigabytes, and two at once mostly get in each other's way.
hold_off_secs= N- The minimum time between two successful builds of one composition. Drift inside that window is put off, not dropped: the first pass after it dispatches. Without this, a 4 GiB image would recompose on every publish that touches its closure. Default 3600.
retry_base_secs= N- Backoff after a failed compose, doubling per consecutive failure. It runs on its own clock, since
hold_off_secscounts from the last published build, and a failure never produces one. Default 3600. max_auto_retries= N- Retries after the first attempt. Default 3.
keep_builds= N- Builds kept per composition, not globally. Image sizes differ by two orders of magnitude, and a global count would let one composition's cadence evict another's history. Default 5.
pkg_binary= "PATH"- The binary a compose job runs. Default
/usr/bin/pkg. resolve_timeout_secs= N- Time limit on the sweep's
pkg compose --dry-run. A timeout reads as unresolvable. Default 600; 0 means no limit. timeout_secs= N- Time limit on one real compose. The job fails when it runs out. Default 7200; 0 means no limit.
sbom= [ format, ... ]- SBOM formats written beside every image:
spdx,cyclonedx. Default both; an empty list writes none. licenses= bool- Whether a license report is written beside every image. Default
true.
The SBOMs and the license report are hashed into the image's metadata, so the build's existing signature covers them too. No second key, no second step.
THE [federation] TABLE
A stage repo that carries a url is remote. This table polls each of those into the local upstream cache, and reaps blobs no upstream references any more. When it's disabled, dispatch base-sync still works as a one-shot.
enabled= bool- Default
true. poll_interval_secs= N- Default 3600. Each poll is a cheap timestamp check; a full fetch only happens when it moved.
gc_interval_secs= N- Default 86400.
retention_days= N- Grace period before an unreferenced blob is reaped. An upstream that drops a package and adds it back shortly after shouldn't cost a re-download. Default 14.
min_free_bytes= N- Below this many free bytes on the cache filesystem, unreferenced blobs are reaped ahead of the grace period. Default 21474836480 (20 GiB); 0 disables it.
THE [trust] TABLE
How fresh the repos this daemon signs have to stay. A repo counts as public once its root key is offline. Internal repos ignore these and keep a far-future expiry. The daemon refuses to start on a TTL of 0 or over ten years, a warning window over 3650 days, or a heartbeat that is not under half of timestamp_ttl_secs.
timestamp_ttl_secs= N- Default 345600 (4 days).
metadata_ttl_secs= N- Lifetime of
targets.jsonandsnapshot.json. Default 31536000 (365 days). heartbeat_interval_secs= N- How often every public repo's timestamp is re-signed. Default 21600; 0 disables it.
root_expiry_warn_days= N- Alert when the newest root expires sooner than this. Default 90.
probe_urls= [ url, ... ]- Public repo base URLs whose published
metadata/timestamp.jsonis probed, to catch a dead mirror or a push that got stuck. The served file must be signed by the timestamp key of a public repo this daemon holds, so probe only repos published from this host. Default empty. probe_interval_secs= N- Default 3600.
THE [alerts] TABLE
Where alerts go. With neither key set, alerts are only logged.
ntfy_url= "URL"- The ntfy topic.
ntfy_token_file= "PATH"- A file holding a bearer token, for a topic that needs one. It is read once at startup, so a rotated token needs a restart. A file that is set but cannot be read, or is empty, stops the daemon from starting.
THE [repro_tests] TABLE
What GET /api/v1/repro-tests/next tells test hosts to rebuild: the builds the realizing stage repos pin now, never a superseded one and never one from a repo that realizes nothing. Failed builds come back for a retest first; the rest is a round robin, builds no host has tried first, then the pass that is oldest. See repro-testing.md in the design docs.
retest_after_secs= N- How long after a build's newest non-reproduced result it is handed out again. Doubles with each such result in a row. Default 21600 (6 h).
retest_max_secs= N- The most that delay grows to. Default 604800 (7 days).
retest_share= F- The most of one answer that may be retests, 0.0 to 1.0. Default 0.5.
max_build_secs= N- Builds whose farm build took longer are never handed out; a host can name one by hand. A host may ask for a lower ceiling, never a higher one. Default 3600.
exclude= [ name, ... ]- Package names never handed out. Default empty.
THE [web] TABLE
Read by dispatch-web(1). The daemon ignores it.
listen= "ADDR:PORT"- The web UI's bind address. Default
127.0.0.1:8081. allowed_hosts= [ name, ... ]Hostheader names accepted on top of localhost, this machine's hostname and its first label, and IP literals. Default empty. Any other name gets a 421 Misdirected Request. That's the guard against DNS rebinding.
Unlike the rest of this file, [web] is strict. An unknown key in it, or an address that doesn't parse, stops dispatch-web from starting. A typo shouldn't silently leave the UI on loopback when you meant to open it up.
LEGACY KEYS
Still parsed, but don't set them on a new host. scratch_buildroot_repo (alias default_buildroot_repo) seeds the scratch floor once, when no repo in the topology carries it. scratch_repo_root and attestation_key_seed_hex are for development only.
NOTES
Outside [web], an unknown key is ignored, not refused. So a misspelled key does nothing at all, quietly. Check a change against the factory file or this page.
EXAMPLES
Serving the web UI on the LAN
[web] listen = "0.0.0.0:8081" allowed_hosts = ["ur-build.lan"]
Anyone who can reach the port can read the dashboard. Every write still needs the caller's own token. Only widen it on a network you trust.
Alerts to ntfy
[alerts] ntfy_url = "https://ntfy.example.org/builds" ntfy_token_file = "/etc/dispatcher/ntfy.token"
Recomposing images at most every six hours
[compose] hold_off_secs = 21600
FILES
/etc/dispatcher/dispatch.toml- This file.
/usr/share/factory/etc/dispatcher/dispatch.toml- The shipped defaults it's seeded from.
/var/lib/dispatcher/topology.toml- Registries and stage repos, written by the daemon.
/var/lib/dispatcher/keys/- Signing seeds and the auth verify key.
ENVIRONMENT
https_proxy,http_proxy,all_proxy,no_proxyThe proxy for the daemon's outbound HTTP, and the hosts that skip it, read the way pkg(1) reads them; the uppercase spellings work too, and pkg.toml(5) has the rules. That's syncing a base or federated repo, fetching the build floor, pinning a federated repo's trust root, downloading a build's upstream sources, the heartbeat's repository probes and ntfy alerts. The
pkg composethe daemon runs inherits them, and also reads/etc/pkg.tomlas thedispatchuser.There's no key for a proxy in this file. Set them on the unit:
# systemctl edit dispatcher.service [Service] Environment=https_proxy=http://proxy.corp.example:3128 Environment=no_proxy=ur-build.lan,127.0.0.1
Each service reads its own environment. designer fetches upstream sources from
designer-run.serviceanddesigner-api.service, so they want the same drop-in. Don't forgetno_proxyfor the build host's own repositories, or local traffic goes the long way round.
SEE ALSO
dispatch-web(1), pkg(1), pkg-compose(1), pkg.toml(5), systemd-tmpfiles(8)
docs/components/dispatcher.md, docs/components/dispatch-web.md, docs/design/deployment-layout.md, docs/design/trust.md