Skip to content

Many hosts, one base

Most flakes with more than one machine grow a mkHost function. It takes a host name, imports a shared module and ./hosts/${name}, sets networking.hostName, and passes values in specialArgs. When there are many hosts, a vars.nix file merges defaults, then category values, then host values. Tools such as easy-hosts add modules per class, per arch and per tag.

# flake.nix: the usual shape
nixosConfigurations = lib.genAttrs (builtins.attrNames (builtins.readDir ./hosts)) (
name:
nixpkgs.lib.nixosSystem {
specialArgs = { inherit inputs; hostname = name; vars = getHostVars name; };
modules = [ ./common.nix ./hosts/${name} { networking.hostName = name; } ];
}
);

In yen a host is an instance of a kind. The kind builds the NixOS system once. Shared configuration is an aspect at the kind. Host values are typed fields with defaults, and every NixOS module of an aspect can read them as an arg. The files below show each part. A table after them maps each usual pattern to its yen form.

modules/fleet.nix
# Hosts are data. One kind builds every NixOS system: no mkHost function, no readDir, no specialArgs.
{
yen,
lib,
inputs,
...
}:
let
inherit (yen.lib) opt;
in
{
yen.kinds.fleet = {
spawns.host = _: yen.host;
sinks.out.reduce = yen.lib.reduce.merge;
};
yen.kinds.host = {
# what a vars.nix file held: typed, with defaults
fields = {
# (1)
system = opt (yen.lib.enum [
"x86_64-linux"
"aarch64-linux"
]) "x86_64-linux";
laptop = opt yen.lib.bool false;
};
sinks.nixos.out = # (2)
{ host, nixos, ... }:
{
nixosConfigurations.${host.name} = inputs.nixpkgs.lib.nixosSystem { modules = nixos; };
};
};
yen.fleet.main = { };
yen.flake = { out, ... }: out;
}

(1) is what a vars.nix file held: typed fields with defaults. (2) is the nixos sink: it builds one NixOS system for each host.

A wrong value fails at the instance that sets it, not deep in nixpkgs.hostPlatform. With yen.host.tower.system = "x86-64-linux", the result has this error, as data:

result
[
{
class = "fixable";
code = "check";
error = "system fails its check";
fix = "give system a value of type one of \"x86_64-linux\", \"aarch64-linux\"";
from = "host:tower";
got = "x86-64-linux";
path = [
"system"
];
want = "one of \"x86_64-linux\", \"aarch64-linux\"";
}
]
modules/base.nix
# What mkHost gave every host, and what easy-hosts gives per arch or per class: aspects at the host kind.
{ yen, ... }:
{
yen.aspects.base = {
at = yen.kinds.host; # (1)
nixos =
{ host, ... }:
{
networking.hostName = host.name; # (2) no specialArgs.hostname: the host is an arg
nixpkgs.hostPlatform = host.system;
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
system.stateVersion = "25.05";
};
};
# per arch: `at` reads a field
yen.aspects.arm = {
at = s: (s.system or null) == "aarch64-linux"; # (3)
nixos.boot.loader.generic-extlinux-compatible.enable = true;
};
yen.aspects.laptop = {
at = s: s.laptop or false;
nixos.services.upower.enable = true;
};
}

(1) makes base active at every host. (2) reads the host through the host arg. (3) makes arm active where the system field is aarch64-linux. See Predicates.

modules/hosts/tower.nix
# One file per host: the instance and its own hardware. A new file is a new host.
{ yen, ... }:
{
yen.host.tower = { }; # (1)
yen.aspects.tower = {
at = yen.host.tower; # (2)
nixos = {
boot.loader.systemd-boot.enable = true;
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
};
};
};
}

In tower.nix, (1) declares the instance and (2) is the aspect for this host only. A real host file usually imports its hardware-configuration.nix in its nixos key, next to the instance.

Usual pattern yen
mkHost "tower" yen.host.tower = { }
builtins.readDir ./hosts import-tree: each file under modules/hosts/ declares its host
specialArgs.hostname, specialArgs.vars { host, ... }: in the NixOS module of any aspect
vars.nix: defaults, category, host fields with defaults; for layered values see Environments
easy-hosts perArch, perClass, perTag an aspect whose at reads a field; see Roles from data
nixos-rebuild switch --flake .#tower the same: the output name is the instance name
ContributeCommunitySponsor