Multi-host NixOS config using flakes. Every host shares common/ (users,
package groups, services, home-manager) so package versions stay identical
machine to machine; per-host files hold hostname + hardware specifics.
flake.nix— pinsnixpkgs(release branch) + home-manager;flake.lockis the version pin. Upgrade on purpose:nix flake lock --update-input nixpkgs, rebuild all hosts, commit. Also pinsnixpkgs-unstable(rolling) for groups listed inperry.unstableGroups; refresh withnix flake update nixpkgs-unstable. Also defineshomeConfigurations.perry-mac, the Mac entry point (see Mac).common/common.nix— shared users, services, networking, home-manager wiring, theperry.systemGroupsselector, and theperry.unstableGroupsselector (same group names served from the rolling unstable branch).common/pkg-groups.nix— named package groups (core,dev,net,langs,ai,containers,tools,backupare safe on any host;browsers,graphics,media,office,dev-gui,remote,terminals,chatare desktop-only). A host picks its set withperry.systemGroups; default is all of them. Headless servers drop the GUI groups (see below).ai(herdr) is nixpkgs-unstable-only — it must be inperry.unstableGroups, not the stable set. The only packages outside the groups:kdePackages.kate(per-user viausers.users.perryh.packagesincommon/common.nix) and the home-manager user packages incommon/home.nix.common/home.nix— shared user config via home-manager (zsh, git identity, ripgrep/fzf/eza, and the per-user agent CLIsomp+opencode2 +herdr).pkgs/— repo-local derivations of packages not in nixpkgs (currentlydsh, built from its published npm tarball, andopencode, OpenCode 2 from upstream's published binaries). Pattern: an overlay inpkgs/<name>/default.nixadded tonixpkgs.overlaysincommon/common.nix(and to the flake's pkgs imports), referenced by name in a group or incommon/home.nix.hosts/<name>.nix— per-host: hostname, boot loader, laptop/desktop specifics, andperry.systemGroupsfor that machine.hosts/<name>-hardware.nix— per-host boot/filesystem/disk. Regenerate withsudo nixos-generate-configand copy over; never hand-edit.
perry.systemGroups selects which named groups to install (default = all,
so a desktop needs no assignment):
# desktop — all groups including core (the default; nothing to set)
# headless server — no GUI, keep the base tools
perry.systemGroups = [ "core" "dev" "containers" ];Groups (safe on any host — including headless servers and the Mac):
| group | packages |
|---|---|
core |
gh, neovim, vim, curl, jq |
dev |
git, dust, fd, pnpm, shellcheck, tree, zola |
net |
whois, net-tools, iputils, bind.dnsutils, mtr, netcat, nmap, iperf3, tcpdump |
langs |
go_latest, python3, ruby, rustc, cargo, nodejs_24 |
ai |
herdr (unstable-only), dsh (repo-local) |
containers |
docker |
tools |
p7zip, ethtool, gptfdisk, parted, testdisk, udisks, unzip |
backup |
restic |
Desktop-only (omit on headless hosts):
| group | packages |
|---|---|
browsers |
firefox, brave, google-chrome |
graphics |
gimp, darktable, digikam, imagemagick, kdePackages.marble, kdePackages.skanpage |
media |
ffmpeg, mediainfo, mpv, ffmpegthumbnailer, vlc, yt-dlp, kdePackages.kdenlive |
office |
libreoffice, kdePackages.kcalc |
dev-gui |
vscode-fhs |
remote |
rustdesk-flutter |
terminals |
ghostty |
chat |
vesktop, signal-desktop, slack |
Selecting containers also enables the docker daemon and adds the user to
the docker group; selecting browsers also enables the firefox module.
A host can take whole groups from the rolling nixpkgs-unstable branch
instead of the stable pin, while the OS and everything else stay on the
release branch:
# hosts/<name>.nix — the list both hosts currently track
perry.unstableGroups = [ "terminals" "chat" "ai" "langs" "dev-gui" "remote" "dev" ];The group is then installed exactly once, from unstable (its stable copy is
dropped). Refresh those apps with nix flake update nixpkgs-unstable +
rebuild; remove a group from the list to return it to the stable pin.
browsers is intentionally kept on the stable pin: Brave's Qt shim loads the
session's Qt plugins from QT_PLUGIN_PATH, so it must be built against the
same Qt as Plasma — unstable's Brave (Qt 6.12) aborts at startup against
26.05's Plasma (Qt 6.11). Browsers refresh with the OS via
nix flake lock --update-input nixpkgs.
Note the ai group (herdr) exists only in unstable — hosts that don't list
it in perry.unstableGroups get a clear error if the stable set includes it.
perry-mac (aarch64-darwin) is a standalone home-manager configuration — no
NixOS, so the package groups cannot come from environment.systemPackages and
no group side effect applies (no docker daemon, no firefox module, no restic
service). Deploy with:
home-manager switch --flake ~/git/nixos-config#perry-macWhich groups the Mac gets is set in the homeConfigurations.perry-mac module
in flake.nix: cliGroups comes from the stable pin, cliUnstableGroups
from nixpkgs-unstable, and both expand the same common/pkg-groups.nix
definitions the NixOS hosts use. Currently core net tools backup (stable) +
langs dev (unstable), so the Mac has gh, git, neovim, jq, restic,
the language toolchains, the dev CLIs and the network tooling. Each group
keeps the source the NixOS hosts take it from, so versions stay identical
machine to machine; groups the hosts track from unstable need to be listed in
cliUnstableGroups to stay in step.
Members nixpkgs marks Linux-only (iputils, ethtool, parted, udisks)
are filtered out automatically. The GUI groups stay Linux-only, and
containers / ai are excluded on purpose (no docker daemon on darwin; the
repo-local dsh would need a darwin build — omp, opencode and
herdr already reach the Mac through common/home.nix).
- Clone this repo (flakes are enabled by default on 25.05+).
sudo nixos-generate-config→ copy/etc/nixos/hardware-configuration.nixinto the repo ashosts/<name>-hardware.nix.- No password setup needed:
users.mutableUsers(default) merges with existing accounts and leaves existing passwords alone. (For a brand-new user, sethashedPassword.) - Create
hosts/<name>.nix(copy one) and addnixosConfigurations.<name> = mkHost "<name>";inflake.nix. sudo nixos-rebuild switch --flake <repo-path>#<name>.
First build compiles the whole closure — takes a while. With a flake,
/etc/nixos/configuration.nix is left as-is; the flake in this repo is the
single source of truth.
- Copy
hosts/perry-eb.nix+hosts/perry-eb-hardware.nixto the new name. - Add
nixosConfigurations.<name> = mkHost "<name>";inflake.nix. - Generate hardware on the machine, deploy, commit.
When a package isn't in nixpkgs at all, add a repo-local derivation under
pkgs/<name>/default.nix as an overlay (wired into nixpkgs.overlays in
common/common.nix and the flake's pkgs imports, referenced by name in a group
or in common/home.nix). Today: dsh, the DeepSeek Harness CLI, built with
buildNpmPackage from the published npm tarball; and opencode, OpenCode 2
built from upstream's published per-platform npm binaries (fetchurl + one SRI
hash per system, patchelf'd to the nix loader — refresh recipes in each
default.nix).