<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Parker Jones Dev Blog - homelab</title>
    <subtitle>Dev Blog of Parker Jones</subtitle>
    <link rel="self" type="application/atom+xml" href="https://parkerjones.dev/projects/homelab/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://parkerjones.dev"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-06-26T00:00:00+00:00</updated>
    <id>https://parkerjones.dev/projects/homelab/atom.xml</id>
    <entry xml:lang="en">
        <title>Nix Fleet, Part 3: Fleet-Wide Secrets with agenix</title>
        <published>2026-06-26T00:00:00+00:00</published>
        <updated>2026-06-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://parkerjones.dev/posts/nix-fleet-agenix-secrets/"/>
        <id>https://parkerjones.dev/posts/nix-fleet-agenix-secrets/</id>
        
        <content type="html" xml:base="https://parkerjones.dev/posts/nix-fleet-agenix-secrets/">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nix Fleet, part 3.&lt;&#x2F;strong&gt; &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-one-flake&#x2F;&quot;&gt;Part 1&lt;&#x2F;a&gt; covered the architecture; &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-remote-builders&#x2F;&quot;&gt;Part 2&lt;&#x2F;a&gt; made builds painless. This part handles the thing you must &lt;em&gt;not&lt;&#x2F;em&gt; commit to a public config repo: secrets.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;My whole system is declared in a Git repository, and most of it is public. But a fleet needs secrets — API keys, SMB credentials, a WireGuard config — and those obviously can&#x27;t sit in plaintext next to the rest of the config. The constraint that shapes everything here: I want secrets to be &lt;strong&gt;versioned, shared across every machine, and reproducible&lt;&#x2F;strong&gt;, with the same Git-reviewable discipline as the rest of the system — without any secret ever appearing in plaintext, in the repo, or in the Nix store.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ryantm&#x2F;agenix&quot;&gt;agenix&lt;&#x2F;a&gt; gets me there. Here&#x27;s the setup.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;two-repos-one-boundary&quot;&gt;Two repos, one boundary&lt;&#x2F;h2&gt;
&lt;p&gt;The cleanest decision I made was to put secrets in their &lt;strong&gt;own separate repo&lt;&#x2F;strong&gt; (&lt;code&gt;nix-secrets&lt;&#x2F;code&gt;) and pull it into the system flake as an input. I keep my real one private, but I&#x27;ve published a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;nix-secrets&quot;&gt;public example repo&lt;&#x2F;a&gt; so you can see the shape:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;# flake.nix
inputs.secrets = {
  url = &amp;quot;git+ssh:&#x2F;&#x2F;git@github.com&#x2F;parallaxisjones&#x2F;nix-secrets.git&amp;quot;;
  flake = false;
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That boundary pays off operationally: I can lock or revoke access to the secrets repo independently of the (public) config, rotate recipients without touching application code, and grant a machine read-only deploy access to just the secrets it needs. The public config can stay public because the sensitive bits live behind a separate door.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-agenix-encrypts&quot;&gt;How agenix encrypts&lt;&#x2F;h2&gt;
&lt;p&gt;agenix is &lt;code&gt;age&lt;&#x2F;code&gt; encryption wired into Nix. You declare, per secret, &lt;em&gt;which identities are allowed to decrypt it&lt;&#x2F;em&gt;. That declaration lives in &lt;code&gt;secrets.nix&lt;&#x2F;code&gt; in the private repo:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;let
  # macOS (pjones) and NixOS (parallaxis) decrypt with the
  # same ~&#x2F;.ssh&#x2F;parallaxis identity.
  pjones = &amp;quot;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...oqt7Aq&amp;quot;;
  users  = [ pjones ];
in
{
  &amp;quot;openai-key.age&amp;quot;.publicKeys        = users;
  &amp;quot;anthropic-api-key.age&amp;quot;.publicKeys = users;
  &amp;quot;datadog-api-key.age&amp;quot;.publicKeys   = users;
  &amp;quot;datadog-app-key.age&amp;quot;.publicKeys   = users;
  &amp;quot;smb-credentials.age&amp;quot;.publicKeys   = users;
  &amp;quot;protonvpn-wireguard.age&amp;quot;.publicKeys = users;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Each &lt;code&gt;.age&lt;&#x2F;code&gt; file is encrypted to the public keys listed. The matching private identity — and &lt;em&gt;only&lt;&#x2F;em&gt; that identity — can decrypt it. The encrypted files are safe to commit (to the private repo); the identity never is.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;one-identity-for-the-whole-fleet&quot;&gt;One identity for the whole fleet&lt;&#x2F;h2&gt;
&lt;p&gt;Notice that &lt;code&gt;users&lt;&#x2F;code&gt; is a single key. That&#x27;s deliberate: &lt;strong&gt;both my macOS and NixOS machines decrypt with the same &lt;code&gt;~&#x2F;.ssh&#x2F;parallaxis&lt;&#x2F;code&gt; ed25519 identity.&lt;&#x2F;strong&gt; agenix can use an &lt;code&gt;age&lt;&#x2F;code&gt;-native key (&lt;code&gt;age-keygen&lt;&#x2F;code&gt;) or reuse an existing SSH ed25519 key via &lt;code&gt;age-plugin-ssh&lt;&#x2F;code&gt;; I reuse the SSH key so there&#x27;s exactly one secret-decrypting identity to manage across the fleet.&lt;&#x2F;p&gt;
&lt;p&gt;The trade-off is real and worth stating: one identity is one thing to protect and one thing to rotate, but it&#x27;s also a single point of compromise. For a personal fleet that&#x27;s the right call — fewer keys means I actually keep the rotation story straight instead of letting five per-host keys drift. If this were a team, I&#x27;d encrypt each secret to multiple recipients (one per person&#x2F;host) so revoking one doesn&#x27;t force re-keying everyone. agenix supports exactly that; it&#x27;s just a longer &lt;code&gt;publicKeys&lt;&#x2F;code&gt; list.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-decryption-works-at-runtime&quot;&gt;How decryption works at runtime&lt;&#x2F;h2&gt;
&lt;p&gt;This is the part that makes agenix better than &quot;stash a &lt;code&gt;.env&lt;&#x2F;code&gt; somewhere.&quot; At activation time, NixOS&#x2F;nix-darwin decrypts each declared secret to a path under &lt;code&gt;&#x2F;run&lt;&#x2F;code&gt; (tmpfs), owned by the user&#x2F;service that needs it, with the permissions you specify. The decrypted value:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;never lands in the Nix store&lt;&#x2F;strong&gt; (which is world-readable and gets copied to your binary cache),&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;never sits in the repo&lt;&#x2F;strong&gt; in plaintext,&lt;&#x2F;li&gt;
&lt;li&gt;exists only on the machine authorized to decrypt it, only while the system is running.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So a service reads its API key from a file at &lt;code&gt;&#x2F;run&#x2F;agenix&#x2F;...&lt;&#x2F;code&gt;, that file only exists because &lt;em&gt;this&lt;&#x2F;em&gt; machine holds the identity that can decrypt it, and the policy for who-can-read-what is reviewable in &lt;code&gt;secrets.nix&lt;&#x2F;code&gt;. Secrets management becomes declarative and auditable, like the rest of the config.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bootstrapping-a-brand-new-machine&quot;&gt;Bootstrapping a brand-new machine&lt;&#x2F;h2&gt;
&lt;p&gt;The chicken-and-egg problem: a fresh machine needs secrets to be useful, but doesn&#x27;t yet have the identity to decrypt them. My flake exposes helper apps for exactly this lifecycle:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;nix run .#create-keys          # generate keys on a new machine
nix run .#copy-keys            # place an identity where agenix expects it
nix run .#check-keys           # verify the identity can decrypt
nix run .#install-with-secrets # first install WITH secrets provisioned
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;install-with-secrets&lt;&#x2F;code&gt; is the interesting one. On a first NixOS install it runs &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-helios64-disko&#x2F;&quot;&gt;disko&lt;&#x2F;a&gt; to partition, stages the repo to the new root, and installs via the flake — and crucially, it relies on &lt;strong&gt;SSH agent forwarding&lt;&#x2F;strong&gt; so the installer can fetch the private &lt;code&gt;secrets&lt;&#x2F;code&gt; input without ever copying a key onto the installer media. Nothing persistent is left behind on the installer once it&#x27;s done. The machine comes up on first boot already matching repo state, with its secrets provisioned where they&#x27;re declared.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-this-beats-the-alternatives&quot;&gt;Why this beats the alternatives&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;vs. plaintext &lt;code&gt;.env&lt;&#x2F;code&gt; &#x2F; committed config:&lt;&#x2F;strong&gt; secrets are encrypted at rest, never in the repo, never in the store.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;vs. a cloud secrets manager:&lt;&#x2F;strong&gt; no external dependency, no per-machine bootstrapping against a third-party API, and the policy is in Git where I review everything else.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;vs. copying keys around by hand:&lt;&#x2F;strong&gt; the recipient list is declarative; adding or removing who-can-decrypt is a reviewable diff, not a tribal-knowledge ritual.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The throughline of this whole series is &quot;manage it declaratively, in Git, reproducibly.&quot; Secrets are the case where people usually give up and do something ad-hoc. agenix is how I kept them inside the same discipline as everything else. Next, in &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-helios64-disko&#x2F;&quot;&gt;Part 4&lt;&#x2F;a&gt;, I put all of this to work standing up an ARM NAS from bare metal — disko for the disks, agenix for the backup credentials, and &lt;code&gt;nixos-anywhere&lt;&#x2F;code&gt; to tie it together.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;A public example secrets repo is at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;nix-secrets&quot;&gt;&lt;code&gt;parallaxisjones&#x2F;nix-secrets&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;; the fleet config that consumes it is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;dotfiles&quot;&gt;&lt;code&gt;parallaxisjones&#x2F;dotfiles&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;— Parker Jones, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;parkerjones.dev&quot;&gt;parkerjones.dev&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Nix Fleet, Part 4: Installing a Helios64 NAS from Bare Metal with nixos-anywhere and disko</title>
        <published>2026-06-26T00:00:00+00:00</published>
        <updated>2026-06-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://parkerjones.dev/posts/nix-fleet-helios64-disko/"/>
        <id>https://parkerjones.dev/posts/nix-fleet-helios64-disko/</id>
        
        <content type="html" xml:base="https://parkerjones.dev/posts/nix-fleet-helios64-disko/">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nix Fleet, part 4.&lt;&#x2F;strong&gt; The series so far: &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-one-flake&#x2F;&quot;&gt;the one-flake architecture&lt;&#x2F;a&gt;, &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-remote-builders&#x2F;&quot;&gt;remote builders&lt;&#x2F;a&gt;, and &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-agenix-secrets&#x2F;&quot;&gt;fleet-wide secrets&lt;&#x2F;a&gt;. This part is where it all gets pointed at bare metal — a 5-bay ARM NAS, installed declaratively and reproducibly.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wiki.kobol.io&#x2F;helios64&#x2F;intro&#x2F;&quot;&gt;Kobol Helios64&lt;&#x2F;a&gt; is a 5-bay NAS built on a Rockchip RK3399 (&lt;code&gt;aarch64&lt;&#x2F;code&gt;, 4–8 GB RAM). It&#x27;s a lovely, slightly cantankerous little ARM board, and it&#x27;s the storage backbone of my homelab. The goal for this host was the same as every other machine in the fleet: &lt;strong&gt;I should be able to reinstall it from the flake and get an identical result, disks and all.&lt;&#x2F;strong&gt; No clicking through an installer, no hand-partitioning, no &quot;what did I configure last time.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s what &lt;code&gt;disko&lt;&#x2F;code&gt; and &lt;code&gt;nixos-anywhere&lt;&#x2F;code&gt; make possible.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-two-tools&quot;&gt;The two tools&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nix-community&#x2F;disko&quot;&gt;disko&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; turns disk layout into declarative Nix. Instead of running &lt;code&gt;fdisk&lt;&#x2F;code&gt; and &lt;code&gt;mkfs&lt;&#x2F;code&gt; by hand, you &lt;em&gt;describe&lt;&#x2F;em&gt; the partitions, filesystems, and pools you want, and disko makes the disks match. Partitioning becomes config you can review, version, and re-apply.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;numtide&#x2F;nixos-anywhere&quot;&gt;nixos-anywhere&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; installs a NixOS flake onto a remote machine over SSH — it can kexec into an installer, run disko to lay out the disks, and install the full system, all from my workstation.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Together they collapse &quot;install a NAS&quot; into one command:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;nix run github:numtide&#x2F;nixos-anywhere -- --flake .#helios64 &amp;lt;ssh-host&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That partitions the drives per the disko spec, installs the &lt;code&gt;helios64&lt;&#x2F;code&gt; host config from my flake, and reboots into a running NixOS. The disk layout is no longer a one-time manual act I&#x27;d have to remember — it&#x27;s part of the host definition.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-arm-specific-bits&quot;&gt;The ARM-specific bits&lt;&#x2F;h2&gt;
&lt;p&gt;An ARM SBC NAS has a few wrinkles a generic x86 box doesn&#x27;t, and the host config names them explicitly:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;# hosts&#x2F;nixos&#x2F;helios64.nix
{
  networking.hostName = &amp;quot;helios64&amp;quot;;

  # The RK3399 needs its device tree to boot correctly
  hardware.deviceTree.enable = true;
  hardware.deviceTree.name = &amp;quot;rockchip&#x2F;rk3399-kobol-helios64.dtb&amp;quot;;

  services.openssh.enable = true;
  system.stateVersion = &amp;quot;24.11&amp;quot;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;strong&gt;device tree&lt;&#x2F;strong&gt; is the load-bearing line — without &lt;code&gt;rk3399-kobol-helios64.dtb&lt;&#x2F;code&gt;, the board doesn&#x27;t come up right. The boot chain itself is U-Boot&#x2F;Tow-Boot on SD&#x2F;eMMC with the NixOS root on SATA. And because this is &lt;code&gt;aarch64-linux&lt;&#x2F;code&gt;, the system image builds on my x86 box via the emulation I set up in &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-remote-builders&#x2F;&quot;&gt;Part 2&lt;&#x2F;a&gt; — I never need a native ARM build host just for the NAS.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A note on honesty: the committed host config is intentionally minimal right now (it even evaluates as a container in CI to bypass boot&#x2F;filesystem asserts). Storage and services are phased work. So treat this post as the &lt;em&gt;design and install method&lt;&#x2F;em&gt; I&#x27;m executing against, not a claim that every service below is already live. The series documents the build as it happens.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;storage-zfs-and-why&quot;&gt;Storage: ZFS, and why&lt;&#x2F;h2&gt;
&lt;p&gt;The storage decision got its own &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;adr.github.io&#x2F;&quot;&gt;ADR&lt;&#x2F;a&gt; (I keep architecture decision records in the repo — &lt;code&gt;docs&#x2F;adr&#x2F;0001-choose-zfs-for-helios64.md&lt;&#x2F;code&gt;, accepted 2025-08-27). The short version:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Decision: ZFS.&lt;&#x2F;strong&gt; End-to-end checksums, snapshots, &lt;code&gt;send&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;receive&lt;&#x2F;code&gt;, and self-healing are worth a lot for data I actually care about, and NixOS&#x27;s ZFS integration is mature. The cost is memory pressure on a low-RAM ARM board and tighter kernel&#x2F;module version coupling. The alternative considered — &lt;code&gt;mdraid&lt;&#x2F;code&gt; + &lt;code&gt;ext4&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;btrfs&lt;&#x2F;code&gt; — is simpler and lighter but has weaker integrity guarantees. For a NAS, integrity wins.&lt;&#x2F;p&gt;
&lt;p&gt;The pool config reflects the board&#x27;s constraints:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;boot.supportedFilesystems = [ &amp;quot;zfs&amp;quot; ];
services.zfs = {
  autoScrub.enable = true;
  trim.enable = true;
};
# On a 4 GB board, cap the ARC so ZFS doesn&amp;#39;t starve everything else:
# boot.kernelParams = [ &amp;quot;zfs.zfs_arc_max=1073741824&amp;quot; ]; # 1 GiB
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Sensible defaults for the pool (&lt;code&gt;tank&lt;&#x2F;code&gt;): &lt;code&gt;ashift=12&lt;&#x2F;code&gt;, &lt;code&gt;compression=zstd&lt;&#x2F;code&gt;, &lt;code&gt;autotrim=on&lt;&#x2F;code&gt;, &lt;code&gt;atime=off&lt;&#x2F;code&gt; for bulk data, and a topology chosen by disk count — mirror for 2 disks, RAIDZ1 for 3–5, RAIDZ2 when I want to survive two failures. Datasets split by purpose: &lt;code&gt;tank&#x2F;data&#x2F;shares&lt;&#x2F;code&gt; for SMB, &lt;code&gt;tank&#x2F;data&#x2F;media&lt;&#x2F;code&gt; for streaming, &lt;code&gt;tank&#x2F;backups&lt;&#x2F;code&gt; for restic targets (with credentials supplied by &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-agenix-secrets&#x2F;&quot;&gt;agenix&lt;&#x2F;a&gt; — that&#x27;s the payoff of Part 3).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-gotchas-this-board-taught-me&quot;&gt;The gotchas this board taught me&lt;&#x2F;h2&gt;
&lt;p&gt;Real hardware has opinions, and the runbook records them so I don&#x27;t relearn them at 2am:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The 2.5GbE port can be flaky&lt;&#x2F;strong&gt; on some Helios64 units. The mitigation is unglamorous: prefer the 1GbE port, lock the link speed at the switch, and capture &lt;code&gt;dmesg&lt;&#x2F;code&gt; for 24h before trusting it. There&#x27;s an ADR for that decision too (&lt;code&gt;0002-networking-1g-vs-2_5g.md&lt;&#x2F;code&gt;).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ZFS ARC will eat a 4 GB board alive&lt;&#x2F;strong&gt; if you let it. Cap &lt;code&gt;zfs_arc_max&lt;&#x2F;code&gt; to 1–2 GiB and re-evaluate after watching real workload for a week.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;RAIDZ1 + power events is a risk.&lt;&#x2F;strong&gt; A NAS without tested backups isn&#x27;t a backup. The drive-replacement playbook (&lt;code&gt;zpool offline&lt;&#x2F;code&gt; → swap → &lt;code&gt;zpool replace&lt;&#x2F;code&gt; → watch the resilver) lives in the design doc, and restore tests are scheduled quarterly.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;why-the-ceremony-is-the-point&quot;&gt;Why the ceremony is the point&lt;&#x2F;h2&gt;
&lt;p&gt;It would be faster, &lt;em&gt;once&lt;&#x2F;em&gt;, to flash an SD card and click through a setup. But &quot;once&quot; is the trap. The value of the disko + nixos-anywhere + flake approach is the &lt;strong&gt;second&lt;&#x2F;strong&gt; time — when a disk dies, or I want to rebuild on fresh drives, or I&#x27;m reproducing the NAS to test an upgrade. Then the disk layout, the ZFS topology, the device tree, the services, and the secrets are all &lt;em&gt;declared&lt;&#x2F;em&gt;, and the rebuild is a command, not an archaeology project.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s the whole thesis of this series, taken to its hardest case. &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-one-flake&#x2F;&quot;&gt;Part 1&lt;&#x2F;a&gt; claimed one flake could configure every machine — laptop, server, and NAS — reproducibly. The NAS is the machine where reproducibility is least convenient and most valuable, and it&#x27;s the one that proves the model. Same flake, same discipline, all the way down to the spinning rust.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The host config and ADRs referenced here live in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;dotfiles&quot;&gt;&lt;code&gt;parallaxisjones&#x2F;dotfiles&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;— Parker Jones, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;parkerjones.dev&quot;&gt;parkerjones.dev&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>One Flake, Every Machine: a NixOS + macOS Fleet</title>
        <published>2026-06-26T00:00:00+00:00</published>
        <updated>2026-06-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://parkerjones.dev/posts/nix-fleet-one-flake/"/>
        <id>https://parkerjones.dev/posts/nix-fleet-one-flake/</id>
        
        <content type="html" xml:base="https://parkerjones.dev/posts/nix-fleet-one-flake/">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nix Fleet, part 1.&lt;&#x2F;strong&gt; This post is the architecture overview. Later parts go deep on the pieces it depends on: cross-arch remote builders, fleet-wide secrets with agenix, and bare-metal install of a NAS with disko.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Every machine I work on — Linux servers, a NAS, and my M3 MacBook — is configured by &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;dotfiles&quot;&gt;one Git repository&lt;&#x2F;a&gt;. One &lt;code&gt;flake.nix&lt;&#x2F;code&gt; describes all of them: same shell, same tools, same agent skills, same secrets handling, whether the target is &lt;code&gt;x86_64-linux&lt;&#x2F;code&gt;, &lt;code&gt;aarch64-linux&lt;&#x2F;code&gt;, or &lt;code&gt;aarch64-darwin&lt;&#x2F;code&gt;. Adding a machine means adding a host entry; changing my setup everywhere means one commit and a rebuild.&lt;&#x2F;p&gt;
&lt;p&gt;This is the &quot;single source of truth for my computing environment&quot; that Nix promises and rarely delivers without a fight. Here&#x27;s how the repo is laid out and the one trick that makes cross-architecture management actually pleasant.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-shape-of-the-repo&quot;&gt;The shape of the repo&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;nixos-config&#x2F;
├── flake.nix          # inputs + outputs: hosts, devShells, apps
├── hosts&#x2F;
│   ├── nixos&#x2F;         # configuration.nix, hardware-configuration.nix,
│   │                  # helios64.nix (the NAS), profiles&#x2F;
│   └── darwin&#x2F;        # configuration.nix (the Mac)
├── modules&#x2F;
│   ├── shared&#x2F;        # config + secrets shared across OSes
│   ├── nixos&#x2F;         # homelab services, Linux-only config
│   └── darwin&#x2F;        # dock, homebrew casks, home-manager
├── overlays&#x2F;          # package customizations
└── justfile           # deploy &#x2F; check convenience targets
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The split that makes this maintainable: &lt;strong&gt;&lt;code&gt;hosts&#x2F;&lt;&#x2F;code&gt; is per-machine, &lt;code&gt;modules&#x2F;&lt;&#x2F;code&gt; is shared.&lt;&#x2F;strong&gt; A host file is mostly a list of which modules to import plus its hardware specifics. The actual configuration — my packages, dotfiles, services — lives in modules that any host can pull in. When I want a tool on every machine, it goes in &lt;code&gt;modules&#x2F;shared&lt;&#x2F;code&gt;; when it&#x27;s Linux-only, &lt;code&gt;modules&#x2F;nixos&lt;&#x2F;code&gt;; macOS-only, &lt;code&gt;modules&#x2F;darwin&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-s-wired-together&quot;&gt;What&#x27;s wired together&lt;&#x2F;h2&gt;
&lt;p&gt;The flake stitches together the whole modern Nix ecosystem:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;inputs = {
  nixpkgs.url = &amp;quot;github:nixos&#x2F;nixpkgs&#x2F;nixos-unstable&amp;quot;;
  home-manager.url = &amp;quot;github:nix-community&#x2F;home-manager&amp;quot;;
  darwin.url = &amp;quot;github:LnL7&#x2F;nix-darwin&#x2F;master&amp;quot;;   # macOS, declaratively
  nix-homebrew.url = &amp;quot;github:zhaofengli-wip&#x2F;nix-homebrew&amp;quot;;
  disko.url = &amp;quot;github:nix-community&#x2F;disko&amp;quot;;        # declarative partitioning
  fenix.url = &amp;quot;github:nix-community&#x2F;fenix&amp;quot;;         # Rust toolchains
  agenix.url = &amp;quot;github:ryantm&#x2F;agenix&amp;quot;;              # age-encrypted secrets
  secrets.url = &amp;quot;git+ssh:&#x2F;&#x2F;git@github.com&#x2F;parallaxisjones&#x2F;nix-secrets.git&amp;quot;;
  my-skills.url = &amp;quot;github:parallaxisjones&#x2F;skills&amp;quot;;  # my Claude skills
  # ...
};
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A few deliberate choices in there:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;nix-darwin&lt;&#x2F;code&gt; + &lt;code&gt;home-manager&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; mean my Mac is configured by the same mechanisms as my Linux boxes. The dock, defaults, and dotfiles are all declarative.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;nix-homebrew&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; is the pragmatic concession: GUI Mac apps still come from Homebrew casks, but managed &lt;em&gt;through&lt;&#x2F;em&gt; Nix so the cask list is in the repo, not in my shell history.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fenix&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; pins my Rust toolchain so &lt;code&gt;cargo&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;clippy&lt;&#x2F;code&gt; are identical everywhere.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;secrets&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; is a &lt;em&gt;separate private repo&lt;&#x2F;em&gt; pulled in as an input — encrypted secrets are versioned and shared across hosts without living in this (public) config. That&#x27;s its own post.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;my-skills&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; wires my &lt;a href=&quot;&#x2F;posts&#x2F;skills-with-nix&#x2F;&quot;&gt;Claude agent skills&lt;&#x2F;a&gt; into the same declarative system.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-cross-arch-trick-build-where-you-can&quot;&gt;The cross-arch trick: build where you can&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s the problem a mixed fleet runs into immediately: &lt;strong&gt;my macOS laptop cannot build Linux derivations locally.&lt;&#x2F;strong&gt; So how do I deploy to the NixOS box from the Mac? The answer is to build &lt;em&gt;on the target&lt;&#x2F;em&gt; and just orchestrate from the laptop. My &lt;code&gt;justfile&lt;&#x2F;code&gt; deploy recipe:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;make&quot;&gt;# Build + switch the NixOS host remotely over SSH. Builds ON the server
# (--build-host) since a darwin laptop can&amp;#39;t build Linux derivations locally.
deploy host=nixos_host:
    nix run nixpkgs#nixos-rebuild -- switch \
      --flake .#{{nixos_attr}} \
      --target-host parallaxis@{{host}} \
      --build-host  parallaxis@{{host}} \
      --use-remote-sudo
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;--build-host&lt;&#x2F;code&gt; and &lt;code&gt;--target-host&lt;&#x2F;code&gt; both pointing at the server means the laptop never compiles a Linux package — it hands the flake to the server, the server builds and switches, and &lt;code&gt;--use-remote-sudo&lt;&#x2F;code&gt; handles the privilege step. I get to drive everything from my editor on macOS. (Part 2 covers the inverse — building Linux artifacts &lt;em&gt;from&lt;&#x2F;em&gt; the Mac via a remote builder — for when you do want local orchestration of the build itself.)&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s a &lt;code&gt;check&lt;&#x2F;code&gt; target too, for a fast feedback loop that doesn&#x27;t build anything:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;make&quot;&gt;check:
    nix eval .#nixosConfigurations.{{nixos_attr}}.config.system.build.toplevel.drvPath
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Evaluating the derivation path is a near-instant syntax-and-type check of the whole host config — the Nix equivalent of &lt;code&gt;tsc --noEmit&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-gotcha-that-cost-me-time&quot;&gt;The gotcha that cost me time&lt;&#x2F;h2&gt;
&lt;p&gt;My &lt;code&gt;nixosConfigurations&lt;&#x2F;code&gt; are keyed by &lt;strong&gt;system string&lt;&#x2F;strong&gt;, not hostname. So the deploy target is &lt;code&gt;.#x86_64-linux&lt;&#x2F;code&gt;, &lt;em&gt;not&lt;&#x2F;em&gt; &lt;code&gt;.#nixos&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;make&quot;&gt;nixos_attr := &amp;quot;x86_64-linux&amp;quot;   # NOT the hostname
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If you key your configs this way and then try &lt;code&gt;nixos-rebuild --flake .#nixos&lt;&#x2F;code&gt;, you get a confusing &quot;attribute not found&quot; with no hint about why. Worth a comment in your own repo so future-you doesn&#x27;t lose twenty minutes to it. (I did.)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-it-s-worth-the-upfront-cost&quot;&gt;Why it&#x27;s worth the upfront cost&lt;&#x2F;h2&gt;
&lt;p&gt;Nix has a real learning tax, and a single-machine setup rarely justifies it. A &lt;em&gt;fleet&lt;&#x2F;em&gt; does. The payoffs that keep me here:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Small, frequent, reversible changes.&lt;&#x2F;strong&gt; Every change is a commit; every rebuild is a new generation I can roll back to atomically. I deploy config changes the way I deploy code.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;New machines reach parity from the flake.&lt;&#x2F;strong&gt; No &quot;setup day.&quot; The repo &lt;em&gt;is&lt;&#x2F;em&gt; the setup.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;One mental model across operating systems.&lt;&#x2F;strong&gt; I stopped maintaining a separate pile of macOS hacks and a separate pile of Linux hacks. It&#x27;s all modules.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The roadmap from here — and the next posts in this series — is the interesting infrastructure underneath: &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-remote-builders&#x2F;&quot;&gt;&lt;strong&gt;remote builders&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt; so cross-compilation stops being a chore (part 2), &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-agenix-secrets&#x2F;&quot;&gt;&lt;strong&gt;agenix&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt; for secrets that are versioned and fleet-wide without ever sitting in plaintext (part 3), and standing up a &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-helios64-disko&#x2F;&quot;&gt;&lt;strong&gt;Helios64 NAS&lt;&#x2F;strong&gt;&lt;&#x2F;a&gt; from bare metal with &lt;code&gt;disko&lt;&#x2F;code&gt; (part 4). The overview is the boring part. The machinery is where Nix gets fun.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The full fleet configuration lives in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;dotfiles&quot;&gt;&lt;code&gt;parallaxisjones&#x2F;dotfiles&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;— Parker Jones, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;parkerjones.dev&quot;&gt;parkerjones.dev&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Nix Fleet, Part 2: Remote Builders — Never Compile Linux on a Mac Again</title>
        <published>2026-06-26T00:00:00+00:00</published>
        <updated>2026-06-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://parkerjones.dev/posts/nix-fleet-remote-builders/"/>
        <id>https://parkerjones.dev/posts/nix-fleet-remote-builders/</id>
        
        <content type="html" xml:base="https://parkerjones.dev/posts/nix-fleet-remote-builders/">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nix Fleet, part 2.&lt;&#x2F;strong&gt; &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-one-flake&#x2F;&quot;&gt;Part 1&lt;&#x2F;a&gt; laid out the one-flake-every-machine architecture. This part solves the problem that makes a mixed Linux&#x2F;macOS fleet painful: my laptop can&#x27;t build Linux.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;When you develop NixOS configs on a Mac, you hit a wall fast: an ARM macOS machine cannot build &lt;code&gt;x86_64-linux&lt;&#x2F;code&gt; or &lt;code&gt;aarch64-linux&lt;&#x2F;code&gt; derivations locally. Nothing about Apple Silicon produces Linux binaries. So either you accept that you can only iterate on Linux configs &lt;em&gt;from&lt;&#x2F;em&gt; a Linux machine, or you set up a remote builder and make the problem disappear.&lt;&#x2F;p&gt;
&lt;p&gt;I set up the remote builder. Now my M3 laptop offloads every Linux build to a beefy x86_64 NixOS box, and from where I sit it&#x27;s transparent — &lt;code&gt;nix build&lt;&#x2F;code&gt; just works, the fans on the laptop stay quiet, and the heavy lifting happens on a machine designed for it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-shape-of-the-solution&quot;&gt;The shape of the solution&lt;&#x2F;h2&gt;
&lt;p&gt;There are two roles:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;builder&lt;&#x2F;strong&gt; — my always-on x86_64 NixOS box (SSH alias &lt;code&gt;nixos&lt;&#x2F;code&gt;). It has the cores, the RAM, and the right architecture.&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;strong&gt;controller&lt;&#x2F;strong&gt; — my M3 MacBook. It evaluates the flake and orchestrates, but delegates the actual compilation.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The trick that makes this clean: configure the controller so that it does &lt;strong&gt;zero&lt;&#x2F;strong&gt; local jobs and ships everything to the builder.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;builder-side-accept-work-and-emulate-arm&quot;&gt;Builder side: accept work, and emulate ARM&lt;&#x2F;h2&gt;
&lt;p&gt;The builder needs to be reachable over SSH and willing to build for the architectures I care about. The one non-obvious piece is ARM: I also run an &lt;code&gt;aarch64-linux&lt;&#x2F;code&gt; machine (a &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-helios64-disko&#x2F;&quot;&gt;Helios64 NAS&lt;&#x2F;a&gt;), and rather than stand up a separate ARM builder, I let the x86 box build ARM derivations through binfmt&#x2F;QEMU emulation:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;# on the x86_64 builder
boot.binfmt.emulatedSystems = [ &amp;quot;aarch64-linux&amp;quot; ];
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That one line registers a QEMU user-mode handler so the x86 host transparently runs &lt;code&gt;aarch64&lt;&#x2F;code&gt; build steps. It&#x27;s slower than native ARM (more on that below), but it means &lt;em&gt;one&lt;&#x2F;em&gt; builder covers my entire Linux fleet across both architectures.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;controller-side-offload-everything&quot;&gt;Controller side: offload everything&lt;&#x2F;h2&gt;
&lt;p&gt;On the Mac, I point Nix at the builder and tell it not to build anything itself:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;# &#x2F;etc&#x2F;nix&#x2F;nix.conf (or the equivalent NixOS&#x2F;darwin option)
builders = @&#x2F;etc&#x2F;nix&#x2F;machines
builders-use-substitutes = true
max-jobs = 0
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;# &#x2F;etc&#x2F;nix&#x2F;machines
nixos x86_64-linux &#x2F; - 8 1 kvm,big-parallel
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The magic value is &lt;strong&gt;&lt;code&gt;max-jobs = 0&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;. It tells the local machine to run zero build jobs — so every derivation that isn&#x27;t already in a binary cache &lt;em&gt;must&lt;&#x2F;em&gt; go to a remote builder. There&#x27;s no &quot;sometimes it builds locally and melts the laptop&quot; ambiguity; local builds are simply off. &lt;code&gt;builders-use-substitutes = true&lt;&#x2F;code&gt; lets the builder pull from binary caches directly instead of having the controller ferry everything, which saves a lot of pointless data movement.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;&#x2F;etc&#x2F;nix&#x2F;machines&lt;&#x2F;code&gt; line reads as: builder &lt;code&gt;nixos&lt;&#x2F;code&gt;, builds &lt;code&gt;x86_64-linux&lt;&#x2F;code&gt;, default SSH key, up to &lt;code&gt;8&lt;&#x2F;code&gt; parallel jobs, speed factor &lt;code&gt;1&lt;&#x2F;code&gt;, and it supports the &lt;code&gt;kvm&lt;&#x2F;code&gt; and &lt;code&gt;big-parallel&lt;&#x2F;code&gt; features (so it&#x27;ll accept jobs that need KVM or heavy parallelism).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;verifying-it-actually-offloads&quot;&gt;Verifying it actually offloads&lt;&#x2F;h2&gt;
&lt;p&gt;The failure mode here is silent: you think you&#x27;re offloading and you&#x27;re not. So verify. First, list what the flake even knows how to build:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;nix eval .#nixosConfigurations --apply builtins.attrNames
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then kick off a real Linux build from the Mac and watch &lt;em&gt;the builder&lt;&#x2F;em&gt;, not the laptop:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;nix build .#nixosConfigurations.&amp;lt;host&amp;gt;.config.system.build.toplevel
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If it&#x27;s working, the laptop sits nearly idle while the x86 box lights up. If &lt;code&gt;max-jobs&lt;&#x2F;code&gt; is misconfigured, you&#x27;ll feel it — the Mac will either try (and fail, for Linux) or grind. Watching &lt;code&gt;nproc&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;free -h&lt;&#x2F;code&gt;&#x2F;build load on the builder during a build is the quickest confirmation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-builds-go-wrong&quot;&gt;When builds go wrong&lt;&#x2F;h2&gt;
&lt;p&gt;Offloading introduces its own failure modes, and I keep a short runbook for them:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OOM on the builder&lt;&#x2F;strong&gt;, especially during ARM-emulated builds, which are memory-hungry. Mitigations: serialize with &lt;code&gt;--option max-jobs 1 --option cores 2&lt;&#x2F;code&gt;, or add temporary &lt;code&gt;zram&lt;&#x2F;code&gt; swap:&lt;pre&gt;&lt;code data-lang=&quot;nix&quot;&gt;{ zramSwap = { enable = true; memoryPercent = 50; }; }
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Inspecting a failed derivation&lt;&#x2F;strong&gt; without re-running the whole build:&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;nix log &#x2F;nix&#x2F;store&#x2F;&amp;lt;hash&amp;gt;-&amp;lt;drv&amp;gt;.drv | less -R
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Checking effective settings&lt;&#x2F;strong&gt; when something isn&#x27;t offloading as expected:&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;nix show-config | egrep &amp;#39;^(cores|max-jobs|builders|system-features|substituters)&amp;#39;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-honest-trade-off&quot;&gt;The honest trade-off&lt;&#x2F;h2&gt;
&lt;p&gt;Emulated ARM builds are &lt;em&gt;slow&lt;&#x2F;em&gt; — QEMU user-mode emulation has real overhead, and a big &lt;code&gt;aarch64&lt;&#x2F;code&gt; derivation built on x86 can take noticeably longer than it would native. For my fleet that&#x27;s an acceptable price: I build ARM rarely (the NAS config doesn&#x27;t change often), and the alternative is maintaining a second physical ARM builder. If you&#x27;re iterating heavily on ARM, buy or borrow a native &lt;code&gt;aarch64&lt;&#x2F;code&gt; builder and add it to &lt;code&gt;&#x2F;etc&#x2F;nix&#x2F;machines&lt;&#x2F;code&gt; as a second entry — the controller config doesn&#x27;t otherwise change. The architecture scales to N builders the same way it handles one.&lt;&#x2F;p&gt;
&lt;p&gt;The win is the same one Part 1 was about: I get to live entirely in macOS, drive my whole Linux fleet from there, and never once wait on my laptop to compile something it was never meant to compile. &lt;a href=&quot;&#x2F;posts&#x2F;nix-fleet-agenix-secrets&#x2F;&quot;&gt;Part 3&lt;&#x2F;a&gt; tackles the next fleet-wide concern — secrets — including how a first-boot install fetches them securely over the same SSH trust I just set up here.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;The builder&#x2F;controller config is part of my &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;parallaxisjones&#x2F;dotfiles&quot;&gt;&lt;code&gt;parallaxisjones&#x2F;dotfiles&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; repo.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;— Parker Jones, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;parkerjones.dev&quot;&gt;parkerjones.dev&lt;&#x2F;a&gt;&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
