Why this matters
Channels are mutable; a flake pins nixpkgs and home-manager in flake.lock so laptop and VPS are built from the same revision. One repo, two hosts, shared modules. nixos-rebuild switch --flake .#laptop is the whole deploy command. You can evaluate the flake on Arch now, before NixOS exists anywhere.
Concept
nixosSystem
nixpkgs.lib.nixosSystem is a thin wrapper over nixos/lib/eval-config.nix from Anatomy of configuration.nix. It is in the flake's lib output, not in pkgs.lib; verified: (builtins.getFlake "nixpkgs").lib ? nixosSystem is true.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, home-manager, ... }@inputs:
{
nixosConfigurations = {
laptop = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [
./hosts/laptop/configuration.nix
./modules/common.nix
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.extraSpecialArgs = { inherit inputs; };
home-manager.users.dawit = import ./home/dawit.nix;
}
];
};
vps = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [
./hosts/vps/configuration.nix
./modules/common.nix
];
};
};
};
}Each host is one nixosSystem call with its own module list. Shared behaviour lives in modules/common.nix and is imported by both. Host-specific files hold hardware-configuration.nix, hostname and the hardware-bound options.
specialArgs: handing inputs to modules
Modules receive { config, lib, pkgs, ... }. They do not receive your flake inputs unless you pass them. specialArgs adds attributes to every module's argument set, and unlike _module.args it is available during imports resolution, which is why inputs go there.
{ inputs, lib, ... }:
{
networking.hostName = lib.mkDefault "from-${inputs.name}";
}Verified with eval-config.nix and specialArgs = { inputs = { name = "flake"; }; }:
"from-flake"In real use you write imports = [ inputs.agenix.nixosModules.default ]; or environment.systemPackages = [ inputs.something.packages.x86_64-linux.default ];.
home-manager as a NixOS module
home-manager.nixosModules.home-manager makes home-manager.users.<name> an option whose value is a home-manager module, the same home.nix you wrote standalone. The system switch now activates the user environment too; one command, one generation.
useGlobalPkgsmakes home-manager reuse the system'spkgs, so overlays andnixpkgs.configapply once.useUserPackagesinstallshome.packagesto/etc/profiles/per-user/<name>instead of~/.nix-profile.extraSpecialArgsisspecialArgsfor the home-manager module tree.
Verify: these four option names at https://home-manager-options.extranix.com (select the NixOS module). Home-manager source is not available offline here.
Trade-off: a broken home.nix now blocks the system switch. The standalone setup from Install home-manager keeps them independent. Pick one; running both on the same user is state 4 from Migrate from chezmoi.
Commands, run on the NixOS host
sudo nixos-rebuild switch --flake .#laptop
sudo nixos-rebuild test --flake /etc/nixos#laptop--flake PATH#NAME selects nixosConfigurations.NAME. With no #NAME, nixos-rebuild uses the machine's hostname. Keep attribute names equal to hostnames and you never type the suffix.
Updating is nix flake update followed by a test, then switch, then a commit of flake.lock. The lock file is the upgrade; the commit is the rollback.
Try it
- Build a minimal flake on Arch. In a fresh directory with the
vps.nixfrom the previous lesson:
{
inputs.nixpkgs.url = "nixpkgs";
outputs = { self, nixpkgs, ... }@inputs:
{
nixosConfigurations.vps = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [ ./vps.nix ];
};
};
}"nixpkgs" as a URL resolves through your registry to the already-downloaded nixpkgs, so this fetches nothing new.
git init && git add . && nix flake showgit+file:///tmp/learnnix-verify/fl
└───nixosConfigurations
└───vps: NixOS configuration- Evaluate an option through the flake path, exactly as
nixos-rebuildwould:
nix eval --raw .#nixosConfigurations.vps.config.networking.hostNamevps- Inspect what got locked:
jq .nodes.nixpkgs.locked.type flake.lockprints"tarball", because the registry entry points at a channel tarball. Agithub:URL would lock to"github"with arev.
Exercise
Add a second host laptop to the Try-it flake that imports the same vps.nix but overrides the hostname, without editing vps.nix. Evaluate both hostnames.
Solution
{
inputs.nixpkgs.url = "nixpkgs";
outputs = { self, nixpkgs, ... }@inputs:
let
mkHost = extra: nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [ ./vps.nix ] ++ extra;
};
in
{
nixosConfigurations = {
vps = mkHost [ ];
laptop = mkHost [ { networking.hostName = nixpkgs.lib.mkForce "laptop"; } ];
};
};
}vps.nix sets hostName = "vps" as a plain definition (priority 100), so the override needs mkForce. The cleaner long-term fix is mkDefault inside the shared module. nix eval --raw .#nixosConfigurations.laptop.config.networking.hostName prints laptop.
Trap
A new module file that is not git added. The error on Nix 2.35 is explicit, but it arrives after a long evaluation and is easy to misread as a module bug:
error: Path 'extra.nix' in the repository "/tmp/learnnix-verify/fl" is not tracked by Git.
To make it visible to Nix, run:
git -C "/tmp/learnnix-verify/fl" add -N "extra.nix"Flakes copy the git tree, not the working directory. git add -N (intent to add) is enough; you do not have to commit.