Remote Builders
Remote Builders
Binary caches move results. Remote builders move work. The boring default is: evaluate on the laptop, realise on a machine that has CPU, KVM, and the right system, then push the result to the team cache.
A laptop should not compile Chromium. A Darwin laptop cannot natively produce x86_64-linux test VMs. Both problems are remote builders, not “more -j”.
Mental model
| Binary cache | Remote builder | |
|---|---|---|
| Needs the path to already exist? | Yes | No |
| What moves first | NAR (result) | .drv + input paths, then NAR back |
| Trust | Signing keys | SSH user on the builder |
| Typical role | Everyone pulls | One beefy box (or a queue) builds |
laptop --drv--> builder --push--> cache --substitute--> fleet
nix.buildMachines (NixOS) or --builders (CLI) lists machines:
hostName,sshUser,sshKeysystem(x86_64-linux,aarch64-linux, …)protocol(ssh-ngpreferred)maxJobs,speedFactorsupportedFeatures(kvm,big-parallel,nixos-test)
If a derivation requires kvm and the builder does not advertise it, Nix builds locally (or fails). Mysterious local compiles are often a missing feature flag.
ssh-ng is the Nix 2 protocol (one SSH session, multiplexed). Plain ssh (legacy) copies NARs over a new connection per path — slower, more fragile. Prefer protocol = "ssh-ng". mandatoryFeatures is the strict sibling of supportedFeatures: the derivation must have those features or Nix will not pick the machine. Use it so a big-parallel Chromium job cannot land on a 2-core builder that merely listed kvm.
Worked examples
Case 1: NixOS client configuration
Save as builders.nix:
# builders.nix
{
nix.distributedBuilds = true;
nix.settings.builders-use-substitutes = true;
nix.buildMachines = [
{
hostName = "builder.desk.internal";
system = "x86_64-linux";
protocol = "ssh-ng";
sshUser = "nixbuilder";
sshKey = "/root/.ssh/id_builder";
maxJobs = 8;
speedFactor = 4;
supportedFeatures = [ "nixos-test" "big-parallel" "kvm" ];
}
];
}builders-use-substitutes = true lets the builder download from the team cache instead of asking the laptop to upload gcc.
The SSH key is a host key for root (the daemon builds as root’s SSH). Protect it with sops. nixbuilder on the far side must be a trusted Nix user.
sudo nixos-rebuild switchCase 2: One-shot without a module
nix build .#desk-api --max-jobs 0 \
--builders 'ssh-ng://nixbuilder@builder.desk.internal x86_64-linux - 8 1 kvm,big-parallel,nixos-test'--max-jobs 0 means “do not build locally.” If the builder is down, the command fails instead of melting the laptop.
Case 3: Builder host — NixOS
Save as builder-host.nix:
# builder-host.nix
{ pkgs, ... }:
{
users.users.nixbuilder = {
isNormalUser = true;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDeskLaptopRootBuilderKey desk-laptop"
];
};
nix.settings.trusted-users = [ "root" "nixbuilder" ];
nix.settings.max-jobs = 8;
services.openssh.enable = true;
}trusted-users is required for nixbuilder to start builds as the daemon. Do not put your whole team in trusted-users on a laptop; do it on the builder.
Test SSH as the key the daemon will use:
sudo ssh -i /root/.ssh/id_builder nixbuilder@builder.desk.internal nix-store --versionIf this asks for a password, the daemon will hang.
Case 4: Darwin → Linux
An Apple silicon laptop producing Linux containers:
# darwin-builders.nix
{
nix.distributedBuilds = true;
nix.buildMachines = [
{
hostName = "linux-builder.desk.internal";
system = "aarch64-linux";
protocol = "ssh-ng";
sshUser = "nixbuilder";
sshKey = "/var/root/.ssh/id_builder";
maxJobs = 8;
supportedFeatures = [ "kvm" "big-parallel" "nixos-test" ];
}
];
}linux-builder can be a VM on the Mac (nix.linux-builder.enable on nix-darwin) or a real ARM server. Prefer a real server for nixosTest (KVM). The VM is fine for GOARCH=arm64 CGO-off Go; it is a poor Hydra.
Root on the laptop does not read ~deskadmin/.ssh/config. Put Host builder in /root/.ssh/config or use an IP in hostName.
Case 5: Features — why tests ran locally
nix build .#checks.x86_64-linux.desk-vm --max-jobs 0If the check is a nixosTest, the builder must list kvm and nixos-test. Missing those, Nix tries the local machine; on Darwin that is a slow VM or a failure.
nix show-config | grep buildersConfirm the live config, not just the .nix file you meant to switch.
The trap
The trap is pointing buildMachines at a machine that is also a substituter with a different nixpkgs. You upload inputs, it rebuilds everything because its store is on 23.11. Same flake.lock everywhere; builder substitutes from the team cache (builders-use-substitutes).
The other trap is user SSH config (~/.ssh/config Host aliases) that root’s daemon does not read. Use hostName IPs or put the alias in /root/.ssh/config.
The boring rule
- Cache for results, builders for first compiles and foreign
system. ssh-ng,builders-use-substitutes,--max-jobs 0when you mean “remote only.”- Advertise
kvm/nixos-test/big-parallelhonestly. - Daemon SSH keys, not your interactive
ssh-agent, unless you designed for that. - Builder nixpkgs + cache = laptop flake lock + team cache.
Try this
sudo sshfrom Case 3 untilnix-store --versionprints without a password.nix build nixpkgs#hello --max-jobs 0 --builders '…'and watch the builder’snix-daemonlog (journalctl -fu nix-daemon).- Remove
kvmfromsupportedFeatures, run anixosTest, and note where it builds. Putkvmback. - After a remote build,
nix path-info -Shr ./resulton the laptop — the NAR should have been copied back. Thencachix pushso the next laptop never builds it.