Framework: Zola
Hosting: Github Pages
Theme: Consoler Dark
Title: parkerjones.dev

One Flake, Every Machine: a NixOS + macOS Fleet

2026-06-26
Parker Jones and Claude Opus 4.8 in homelab
#nix , #nixos , #nix-darwin , #home-manager , #flakes , #infrastructure and #macos
4 minute read

Nix Fleet, part 1. 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.

Every machine I work on — Linux servers, a NAS, and my M3 MacBook — is configured by one Git repository. One flake.nix describes all of them: same shell, same tools, same agent skills, same secrets handling, whether the target is x86_64-linux, aarch64-linux, or aarch64-darwin. Adding a machine means adding a host entry; changing my setup everywhere means one commit and a rebuild.

This is the "single source of truth for my computing environment" that Nix promises and rarely delivers without a fight. Here's how the repo is laid out and the one trick that makes cross-architecture management actually pleasant.

The shape of the repo

nixos-config/
├── flake.nix          # inputs + outputs: hosts, devShells, apps
├── hosts/
│   ├── nixos/         # configuration.nix, hardware-configuration.nix,
│   │                  # helios64.nix (the NAS), profiles/
│   └── darwin/        # configuration.nix (the Mac)
├── modules/
│   ├── shared/        # config + secrets shared across OSes
│   ├── nixos/         # homelab services, Linux-only config
│   └── darwin/        # dock, homebrew casks, home-manager
├── overlays/          # package customizations
└── justfile           # deploy / check convenience targets

The split that makes this maintainable: hosts/ is per-machine, modules/ is shared. 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 modules/shared; when it's Linux-only, modules/nixos; macOS-only, modules/darwin.

What's wired together

The flake stitches together the whole modern Nix ecosystem:

inputs = {
  nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
  home-manager.url = "github:nix-community/home-manager";
  darwin.url = "github:LnL7/nix-darwin/master";   # macOS, declaratively
  nix-homebrew.url = "github:zhaofengli-wip/nix-homebrew";
  disko.url = "github:nix-community/disko";        # declarative partitioning
  fenix.url = "github:nix-community/fenix";         # Rust toolchains
  agenix.url = "github:ryantm/agenix";              # age-encrypted secrets
  secrets.url = "git+ssh://git@github.com/parallaxisjones/nix-secrets.git";
  my-skills.url = "github:parallaxisjones/skills";  # my Claude skills
  # ...
};

A few deliberate choices in there:

The cross-arch trick: build where you can

Here's the problem a mixed fleet runs into immediately: my macOS laptop cannot build Linux derivations locally. So how do I deploy to the NixOS box from the Mac? The answer is to build on the target and just orchestrate from the laptop. My justfile deploy recipe:

# Build + switch the NixOS host remotely over SSH. Builds ON the server
# (--build-host) since a darwin laptop can'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

--build-host and --target-host 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 --use-remote-sudo handles the privilege step. I get to drive everything from my editor on macOS. (Part 2 covers the inverse — building Linux artifacts from the Mac via a remote builder — for when you do want local orchestration of the build itself.)

There's a check target too, for a fast feedback loop that doesn't build anything:

check:
    nix eval .#nixosConfigurations.{{nixos_attr}}.config.system.build.toplevel.drvPath

Evaluating the derivation path is a near-instant syntax-and-type check of the whole host config — the Nix equivalent of tsc --noEmit.

The gotcha that cost me time

My nixosConfigurations are keyed by system string, not hostname. So the deploy target is .#x86_64-linux, not .#nixos:

nixos_attr := "x86_64-linux"   # NOT the hostname

If you key your configs this way and then try nixos-rebuild --flake .#nixos, you get a confusing "attribute not found" with no hint about why. Worth a comment in your own repo so future-you doesn't lose twenty minutes to it. (I did.)

Why it's worth the upfront cost

Nix has a real learning tax, and a single-machine setup rarely justifies it. A fleet does. The payoffs that keep me here:

The roadmap from here — and the next posts in this series — is the interesting infrastructure underneath: remote builders so cross-compilation stops being a chore (part 2), agenix for secrets that are versioned and fleet-wide without ever sitting in plaintext (part 3), and standing up a Helios64 NAS from bare metal with disko (part 4). The overview is the boring part. The machinery is where Nix gets fun.

— Parker Jones, parkerjones.dev