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:
nix-darwin+home-managermean my Mac is configured by the same mechanisms as my Linux boxes. The dock, defaults, and dotfiles are all declarative.nix-homebrewis the pragmatic concession: GUI Mac apps still come from Homebrew casks, but managed through Nix so the cask list is in the repo, not in my shell history.fenixpins my Rust toolchain socargo/clippyare identical everywhere.secretsis a separate private repo pulled in as an input — encrypted secrets are versioned and shared across hosts without living in this (public) config. That's its own post.my-skillswires my Claude agent skills into the same declarative system.
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:
- Small, frequent, reversible changes. 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.
- New machines reach parity from the flake. No "setup day." The repo is the setup.
- One mental model across operating systems. I stopped maintaining a separate pile of macOS hacks and a separate pile of Linux hacks. It's all modules.
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