Skip to main content

Worker Configuration

Upgrading from a single container

If you are still running AURCache as one container, it keeps working through the hybrid compatibility image, which is deprecated. Everything on this page applies to a worker you run yourself.

A build worker is a long-lived process that enrolls with AURCache, claims build jobs, builds each package in its own devtools chroot, and uploads the result. A worker starts from its environment variables. Its policy and tuning settings — concurrency, build limits, cache budgets, timeouts — can then be set for it from AURCache, on the worker's own page, and reach it without a restart. Everything else (its identity, its paths, what a build may reach) stays on the machine.

Seeing what a worker is running​

A worker tells the server which of its settings can be configured, and what each of them resolved to on that machine. Follow a worker from the Workers list to its own page — /worker/<name> — to see both: the value in force, where it came from, and the variable that set it.

Worker names are not required to be unique: a worker is called whatever its machine reports (WORKER_NAME, or the hostname), and a revoked worker keeps its row so old builds still name the machine that ran them. Where two workers share a name the page asks which you meant, and each is also reachable by the fingerprint that is its real identity.

This is where a value that did not parse shows up. WORKER_BUILDDIR_MAX_BYTES set to something the worker cannot read does not stop it starting — it falls back and carries on — but the page then says the value was refused and names what is running instead, and the worker's row is flagged in the list. Before, the only trace was a line in that machine's journal.

Setting values from AURCache​

Each setting on a worker's page has a field. Change as many as you need, then Save: the whole set is saved as one change, so a worker never picks up half of it — fewer builds with more memory each cannot arrive as more builds with more memory each. The CLI does the same:

aurcli worker config 3 # show
aurcli worker config 3 --set concurrency=2 --set build_memory_max=48G
aurcli worker config 3 --reset build_timeout # back to the worker's own

A value is checked against what that worker says it accepts before it is saved, so 1.5 bananas for a size is refused where you typed it. The worker picks the save up on its next heartbeat (every 15 seconds), or when it next checks in if it is offline, and its page then shows the value as set here. When it takes effect depends on the setting, and the page says which:

Takes effectSettings
At onceconcurrency (running builds finish; nothing new starts over the new limit), priority and package affinity, the WORKER_TOTAL_BUILD_* totals
From the next buildper-build limits, build timeout, keyserver — builds already running keep what they started with
At its next passcache budgets and TTLs, build-tree budgets, chroot refresh and poll intervals

The totals apply to builds already running: lowering the total memory below what they are using makes the kernel reclaim and then kill one of them.

A worker can still refuse a value AURCache accepted — a CPU limit on a host whose cgroup cannot take one, say. It then keeps the value it was running (a refusal never loosens a limit), and the page shows the refusal and why.

Removing a value (Reset) returns the setting to the worker's own: its _DEFAULT variable if it has one, otherwise its built-in default. A value set for a setting the worker no longer offers — renamed or removed in an upgrade — is kept and listed separately rather than deleted, so you can clear it.

Values set here are included in a backup (Settings → Backup), with the worker they belong to, and restored with it.

_DEFAULT: a value the server may take over​

Every setting that page lists reads two variables:

In the worker's environmentMeaning
WORKER_CONCURRENCY=4Pin. This machine runs 4, whatever anything else says.
WORKER_CONCURRENCY_DEFAULT=4Default. This machine runs 4 until a value is set for it on the server.

A value set in AURCache beats _DEFAULT but not the pin: the order is pin, then AURCache, then _DEFAULT, then the built-in default. So a machine can keep a value out of AURCache's hands — its memory limit, say — while leaving its concurrency to be managed centrally. A pinned setting's field is disabled, and the page names the variable doing the pinning.

Renaming a variable is the whole of the handover: WORKER_CONCURRENCY=2 becomes WORKER_CONCURRENCY_DEFAULT=2, and the machine keeps running 2 until a value is set for it. The compose files AURCache ships, and those aurcli setup writes, use the _DEFAULT names. A worker older than this feature ignores _DEFAULT and runs its built-in default instead, so upgrade workers before renaming their variables.

Only the settings that page lists read _DEFAULT. The ones that decide what a build can reach — the chroot directory, the bind mounts, the build user — are deliberately not among them: a worker runs devtools as root, so those stay on the machine, where they always were.

Identity and capabilities​

VariableTypeDescriptionDefault
WORKER_NAMEStringName shown on the Workers pagehostname
WORKER_ARCHESStringArchitectures this worker builds, comma or space separatedhost architecture
WORKER_CONCURRENCYIntegerMaximum builds at once1
Raising concurrency

Each build is already parallel — MAKEFLAGS=-j$(nproc) is set inside every build — so WORKER_CONCURRENCY=N runs up to N × CPU-count compiler processes, and each concurrent build additionally holds its own chroot copy and package cache. Raise it when you have memory and disk headroom, not simply because the machine has cores; two or three is a large increase for most hosts.

Building for another architecture​

A worker builds in a chroot of its own architecture, so it builds for that architecture only.

A foreign-architecture worker is the same worker image run emulated, with platform: linux/arm64 (or linux/arm/v7). Docker uses binfmt_misc to register the qemu interpreter for that platform, so from the worker's point of view it is building natively and the build path is identical to a native worker's.

The bundled compose file ships this behind a profile:

docker compose --profile arm up -d

which also runs tonistiigi/binfmt once to register the handlers on the host kernel. Emulated builds are slow, so that worker is configured with WORKER_CONCURRENCY=1; if you later add a native aarch64 machine, give it a higher priority and aarch64 jobs will prefer it, falling back to emulation only when it is busy or offline.

Supported platforms are x86_64, aarch64 and armv7 — the Arch Linux base image the worker is built on exists only for those. Emulation itself only runs one way: qemu user-mode runs on an x86_64 host, so cross builds always start there.

If a worker fails to start with exec container process (missing dynamic library?), its qemu/binfmt setup is misconfigured: binfmt_misc must be enabled in the kernel (cat /proc/sys/fs/binfmt_misc/status), enabling it with mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc if not.

Swarm and other runtimes​

In Docker Swarm the worker is an ordinary service with the same needs: the permissions a chroot build requires — privileged: true, or SYS_ADMIN / SYS_PTRACE with seccomp and AppArmor unconfined — plus a writable tmpfs at /run. Constrain workers to particular nodes with the usual Swarm placement rules, which pairs well with routing when a package must be built on a specific machine.

Routing​

VariableTypeDescriptionDefault
WORKER_PACKAGESStringExact pkgbase names this worker is provisioned forempty
WORKER_PRIORITYIntegerScheduling preference; higher wins0

See Routing builds to specific workers.

Mirrorlist​

By default a worker needs none of this: the server sends the mirrorlist for the build's architecture with each job, and a worker that gets none uses its image's own /etc/pacman.d/mirrorlist.

Set one of these when this worker's mirrors beat the server's — a worker on other hardware, or across a slow link from the mirror the server prefers.

VariableTypeDescriptionDefault
WORKER_MIRRORLIST_SERVERSStringSemicolon-separated mirror URLs, used instead of the server'sempty
WORKER_MIRRORLIST_FILEPathA ready mirrorlist file inside the containerempty

WORKER_MIRRORLIST_SERVERS wins if both are set. Either one makes the worker tell the server not to send a mirrorlist at all, so nothing is transferred that would only be discarded.

See Mirrorlist for the server side.

Build credentials​

VariableTypeDescriptionDefault
WORKER_GIT_SSH_KEYPathSSH key for authenticated sources. When set, no key is generatedgenerate one
WORKER_SSH_KNOWN_HOSTSPathknown_hosts to trust when fetching sourcesaccept on first use
WORKER_BIND_MOUNTSStringExtra host:chroot mounts, comma separatedempty

See Build credentials.

Caches​

Workers keep two caches: downloaded upstream sources (SRCDEST — tarballs and VCS checkouts) and downloaded pacman packages. They are budgeted separately because they differ enormously in size and in what it costs to refill them: a few large VCS checkouts sharing one budget would evict every cached package, leaving each build to re-download its dependencies.

VariableTypeDescriptionDefault
WORKER_CACHE_MAX_SIZESizeTotal cache budget, split evenly between the two pools20G
WORKER_SRCCACHE_MAX_SIZESizeSource budget; overrides half the totalhalf of total
WORKER_PKGCACHE_MAX_SIZESizePackage budget; overrides half the totalhalf of total
WORKER_CACHE_TTLDurationEvict sources unused for this long (0 disables)30d
WORKER_PKGCACHE_TTLDurationEvict packages older than this (0 disables)0
WORKER_BUILDDIR_MAX_BYTESSizeBudget for kept build trees, for packages with a persistent build directory200G
WORKER_BUILDDIR_MIN_FREESizeFree space to keep in the storage pool when deciding whether to drop kept build trees50G

The caches live in the worker's storage pool, in a cache subvolume beside the chroots, so they count against WORKER_DISK_MAX together with the base chroot and the running builds. Each package's sources and each kept build tree is a subvolume of its own: its size is read from btrfs rather than by walking it, and evicting it is instant however many files it holds. A build is given only its own kept tree. The budgets here are what eviction keeps them to between builds; leave room under WORKER_DISK_MAX for at least one build's WORKER_BUILD_DISK_MAX on top of them, or a build may find the pool full before its own quota is reached.

Sizes are read the way coreutils reads them: a plain number is bytes, a bare unit or an iB unit is binary, and a B unit is decimal -- 450G and 450GiB are both 450 GiB, while 450GB is 450 × 10⁹ bytes. Units run from K to T, in any case, with or without a space before them. Setting a budget to 0 disables size-based eviction for that pool.

A value the worker cannot read, here or in any other setting, is logged as a warning at startup and replaced by the default -- check the log after changing one.

Setting both per-pool budgets makes the split ratio configurable without a separate knob for it. A pinned pool always wins: the total is a default for whatever is left unset, not a cap enforced over values you wrote down.

Package TTL defaults to off deliberately. A cached package's timestamp is when it was downloaded, not when it was last used — pacman does not touch it on a cache hit — so age-evicting would discard a package used in every build simply for being old. Size pressure is the honest bound for that pool.

Resource limits​

VariableTypeDescriptionDefault
WORKER_BUILD_MEMORY_MAXSizeMemory each build may use; past it, the kernel kills processes in the buildunlimited
WORKER_BUILD_SWAP_MAXSizeSwap each build may use besides that memory0 when WORKER_BUILD_MEMORY_MAX is set, otherwise unlimited
WORKER_BUILD_CPUSNumberCPU time each build may use, in cores; fractions such as 2.5 are allowedunlimited
WORKER_TOTAL_BUILD_MEMORY_MAXSizeMemory all of this worker's builds may use togetherunlimited
WORKER_TOTAL_BUILD_SWAP_MAXSizeSwap all builds may use together besides that memory0 when WORKER_TOTAL_BUILD_MEMORY_MAX is set, otherwise unlimited
WORKER_TOTAL_BUILD_CPUSNumberCPU time all builds may use together, in coresunlimited

The WORKER_BUILD_* limits apply to each build, so a worker running WORKER_CONCURRENCY builds can use that many times as much. The WORKER_TOTAL_BUILD_* limits bound what the builds use between them. The two nest, and can be combined: with WORKER_BUILD_MEMORY_MAX=32G and WORKER_TOTAL_BUILD_MEMORY_MAX=48G, no single build gets more than 32G, and two running at once share 48G.

A total does not limit how many builds start -- WORKER_CONCURRENCY does. Two builds that each need 30G under a 48G total both start, and the one that allocates past the total is killed. A build can therefore be killed while under its own limit; its failure reason says which limit was reached:

Reason namesWhat was reached
WORKER_BUILD_MEMORY_MAXthe build's own limit
WORKER_TOTAL_BUILD_MEMORY_MAXthe total, shared with the builds running beside it
neithera limit outside the worker: the machine's memory, or one on what runs the worker

Neither limit includes the worker process itself, which sits beside the builds rather than under the total, so reaching the total can only ever stop a build. To bound literally everything, limit what runs the worker instead: MemoryMax=/CPUQuota= on the systemd unit, --memory/--cpus on its container. The totals are applied when the worker starts; changing one takes a restart.

A memory limit limits swap too. On its own, a cgroup's memory limit only bounds RAM: a build over it is pushed out to swap rather than stopped, which on a machine with swap means no limit at all. So setting WORKER_BUILD_MEMORY_MAX also sets the build's swap allowance to zero, and a build that needs more is killed. Set WORKER_BUILD_SWAP_MAX to allow some. The total works the same way, with WORKER_TOTAL_BUILD_SWAP_MAX. A build killed this way fails with a reason that names the limit, rather than a bare exit code.

A CPU limit also limits parallelism. nproc inside the build still counts every core, so the worker lowers MAKEFLAGS, OMP_NUM_THREADS and CARGO_BUILD_JOBS to the number of CPUs allowed -- the smaller of WORKER_BUILD_CPUS and WORKER_TOTAL_BUILD_CPUS when both are set. Without that, a build limited to 6 CPUs on a 24-core machine would start 24 compilers, each holding its own memory. A build that picks its parallelism some other way is still held to the limit, only less efficiently.

The limits are enforced with cgroups -- each build's own, under a builds cgroup that holds the total -- which need the same thing the build's memory figure does: a privileged container, or Delegate=yes on the systemd unit (the packaged unit has it). A worker that has limits configured but cannot enforce them refuses every build and says so at startup, rather than running builds without the limits it was given.

The legacy container builder keeps its own CPU_LIMIT (milli-CPUs) and MEMORY_LIMIT (MB, negative for unlimited). Like the settings above, they are listed on the worker's page and can be set there, and read CPU_LIMIT_DEFAULT and MEMORY_LIMIT_DEFAULT as defaults AURCache may override; the plain names still pin. Its builder image and build directory are not settable from AURCache.

Locations​

VariableTypeDescriptionDefault
WORKER_DATA_DIRPathWorker identity, credentials, chroots/var/lib/aurcache-worker
WORKER_CHROOT_DIRPathWhere the storage pool's image and mount point go (see below)<data dir>/chroot
WORKER_CHROOT_REFRESH_INTERVALDurationHow long a pacman -Syu'd base chroot counts as current (0 refreshes before every build)15m
WORKER_BUILD_USERStringThe user each build runs as. Its primary group must be aurbuild: makechrootpkg carries only the primary group into the chrootbuilder

The base chroot is brought up to date with pacman -Syu before a build, but not more often than WORKER_CHROOT_REFRESH_INTERVAL. The refresh takes around 13 seconds, so paying it per build delayed every build start in a burst; measured over a day on one worker, 23 of 26 refreshes upgraded nothing at all, because Arch's repositories move a few times a day rather than a few times an hour. Lower it if you would rather have the newest dependencies than the fastest starts; 0 refreshes before every build.

WORKER_DATA_DIR must be a persisted volume. It holds the worker's identity and its generated SSH key; if it is lost, the worker re-enrolls as a new, unapproved worker and generates a new key, which the remote will no longer accept.

Disk quota and the storage pool​

The pool's own variables (WORKER_POOL, WORKER_DISK_MAX, WORKER_BUILD_DISK_MAX, WORKER_DISK_RESERVE, WORKER_CHROOT_DIR) are listed in Storage pool.

VariableTypeDescriptionDefault
WORKER_KEEP_FAILEDDurationHow long a failed build's chroot is kept for inspectionunset (keeps nothing)

Every chroot, build and cache lives in the worker's storage pool, a btrfs filesystem with quotas. The kernel enforces both limits as builds write: a build that tries to fill the disk fails with "Disk quota exceeded" and nothing outside the pool is touched. By default the pool is a sparse image file that needs no setup. On ZFS, a zvol is better.

Storage pool covers the three backings (an image file, a zvol or partition, a dedicated btrfs filesystem), their tuning, sizing, and resizing.

When WORKER_KEEP_FAILED is set, a failed build's chroot is kept at <pool>/kept-<id> (with its workdir beside it, and its build tree at <pool>/kept-<id>.build when the build kept one) instead of deleted, so a failure can be inspected in the state it died in. The build's page says where and until when, and shows the one systemd-nspawn command that enters it. Canceled, timed-out and out-of-disk builds are never kept, and one-shot builds neither; a kept build counts against WORKER_DISK_MAX and is deleted early, oldest first, when the pool needs the room.

Timing​

VariableTypeDescriptionDefault
WORKER_BUILD_TIMEOUTDurationKill a build after this long (0 disables), or after the package's job timeout when that is shorter3h
WORKER_POLL_INTERVALDurationTime between job claims when idle10s
WORKER_KEYSERVERStringKeyserver for validpgpkeyshkps://keyserver.ubuntu.com

Durations are a plain number of seconds, or terms with a unit that add up: 90s, 15m, 3h, 1h30m, 2h 30min, 30d, 2w. Units are s, m (minutes), h, d and w, or spelled out (min, hours, days), in any case. Like sizes, a value that cannot be read is logged at startup and the default used.

A build stops at the shorter of WORKER_BUILD_TIMEOUT and its package's job timeout (JOB_TIMEOUT, an hour by default, which a package can set for itself), and its log says which it ran into. Very large packages need both raised: an hour, or three, is generous for ordinary packages and nowhere near enough for something like a browser or a game engine. Raising the package's own job timeout leaves every other package's as it is.

Connecting to AURCache​

VariableTypeDescriptionDefault
AURCACHE_URLStringWorker protocol endpoint (HTTPS + mTLS)https://localhost:8083
AURCACHE_SERVER_CA_FINGERPRINTStringPin the server's CA (SHA-256 hex)trust on first use
AURCACHE_ENROLLMENT_DIRPathShared volume for zero-touch approval/enroll
AURCACHE_ENROLLMENT_TOKENStringShared secret for non-interactive approvalnone
AURCACHE_REPO_URLStringThe package repository's base URL as this worker reaches it, in place of the one the server gives every workerthe server's
AURCACHE_REPO_HOSTStringOnly the host of that URL, for a worker that reaches the repository at another addressthe server's

The server renders one repository URL for every worker from its AURCACHE_PUBLIC_URL. A worker that reaches the repository another way — inside the compose network rather than through a reverse proxy, say — sets one of the last two.

Server-side settings​

These are set on the AURCache server, not the worker.

VariableTypeDescriptionDefault
AURCACHE_WORKER_PORTIntegerPort for the worker protocol listener8083
AURCACHE_WORKER_IMAGEStringWorker image named in the copy-and-run command the Workers page shows when no worker exists yetghcr.io/lukas-heiligenbrunner/aurcache-worker:latest
WORKER_SPILL_DELAYDurationHow long before priority stops holding a job back60s
WORKER_LIVENESS_TIMEOUTDurationHow long before a quiet worker stops counting as available60s
MAX_ATTEMPTSIntegerRequeues before a build is failed for good3
WORKER_CERT_VALIDITY_DAYSIntegerLifetime of an issued worker certificate3650

Set AURCACHE_WORKER_IMAGE when you publish the worker image to your own registry or want the suggested command to pin a tag — it only changes what that one command displays, nothing about how workers connect.

Worker certificates are long-lived on purpose. A certificate grants nothing on its own — authorization is the worker's approval status, which an operator can revoke at any moment — so a short lifetime would add no security and would eventually break a worker with no renewal path.