Multi-host flake — one repo, many machines#
getting-started.md promises “one repo, many hosts”. This page is the mechanics of that promise — what flake.nix actually looks like when you have a VirtualBox VM, a libvirt VM on Kali, and an external-SSD NixOS all expressed in the same repo, with a home.nix that travels everywhere.
Prereq: read nix-language.md if function-and-attribute-set syntax isn’t fluent yet; read home-manager-modes.md to understand when we use homeConfigurations.* vs home-manager.users.* inside a NixOS config.
In this page#
The shape of a maturing repo#
~/nixos-config/
├── flake.nix
├── flake.lock
├── home.nix ← portable user config, works on any host
├── hosts/
│ ├── desktop-vbox/
│ │ ├── configuration.nix ← VirtualBox VM on Windows host
│ │ └── hardware-configuration.nix
│ ├── kali-vm/
│ │ ├── configuration.nix ← libvirt VM on Kali host
│ │ └── hardware-configuration.nix
│ └── portable-ssd/
│ ├── configuration.nix ← external-SSD NixOS, real hardware
│ └── hardware-configuration.nix
└── modules/
├── common.nix ← keyboard, locale, timezone, system packages everyone wants
├── dev.nix ← git, docker, direnv, language toolchains
├── kde.nix ← Plasma 6 desktop
├── fish.nix ← fish + prompt + plugins
└── pentest.nix ← nmap, metasploit, wireshark, ... (for the Kali-replacement future)
Three claims this structure makes, worth reading carefully:
home.nixis at the root. It is the same file for every host — the common user-level config you want identically everywhere.Each
hosts/<name>/configuration.nixis thin. It only declares what makes this host unique (its bootloader style, its hostname, its networking, which shared modules it imports). The bulk lives inmodules/.hardware-configuration.nixis generated per-host bynixos-generate-configat install time and committed to its host directory. Not shared across hosts.
Smaller repos can collapse this: put configuration.nix at the root, keep only one host. But as soon as there are two, the tree above pays off.
A concrete flake.nix#
{
description = "My portable NixOS + home-manager setup";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
# Optional extras — uncomment as you adopt them.
# disko.url = "github:nix-community/disko";
# disko.inputs.nixpkgs.follows = "nixpkgs";
# impermanence.url = "github:nix-community/impermanence";
};
outputs = inputs@{ self, nixpkgs, home-manager, ... }:
let
system = "x86_64-linux";
# Factor out the common bits of a nixosSystem invocation so each host is a one-liner.
mkHost = hostName: extraModules:
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = { inherit inputs; };
modules = [
./hosts/${hostName}/configuration.nix
./modules/common.nix
home-manager.nixosModules.home-manager
{
networking.hostName = hostName;
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.alice = import ./home.nix;
}
] ++ extraModules;
};
in
{
nixosConfigurations = {
desktop-vbox = mkHost "desktop-vbox" [
./modules/kde.nix
./modules/dev.nix
./modules/fish.nix
];
kali-vm = mkHost "kali-vm" [
./modules/dev.nix
./modules/fish.nix
# no KDE — this VM runs headless, accessed over SSH
];
portable-ssd = mkHost "portable-ssd" [
./modules/kde.nix
./modules/dev.nix
./modules/fish.nix
# Future: ./modules/pentest.nix
];
};
# Also expose a standalone home-manager config, for hosts that aren't NixOS (Kali, WSL2-Debian).
homeConfigurations."alice" = home-manager.lib.homeManagerConfiguration {
pkgs = import nixpkgs { inherit system; config.allowUnfree = true; };
modules = [ ./home.nix ];
};
};
}
Worth calling out:
mkHostis a tiny local helper that captures “the same five lines I’d otherwise repeat three times”. It’s plain Nix — no magic.specialArgs = { inherit inputs; };passes the wholeinputsattribute set down to every module as theinputsargument. Modules can then doinputs.impermanence.nixosModules.impermanencewithout re-importing the flake.networking.hostNameis set in the flake, not in the host’sconfiguration.nix. This avoids the classic “I copied the VM and forgot to change the hostname” bug.homeConfigurations."alice"coexists with the NixOS configs. On Kali and WSL2-Debian you callhome-manager switch --flake ~/nixos-config#alice; on the NixOS hosts the home-manager module handles it automatically as part ofnixos-rebuild switch. Samehome.nix, two entry points.
Per-host directory contents#
A typical host’s configuration.nix stays small — the heavy lifting is in modules/. Example for hosts/desktop-vbox/configuration.nix:
{ config, pkgs, lib, ... }:
{
imports = [ ./hardware-configuration.nix ];
# Bootloader (VirtualBox uses BIOS by default unless EFI is enabled in the VM settings)
boot.loader.grub = {
enable = true;
device = "/dev/sda";
useOSProber = false;
};
# VirtualBox Guest Additions for seamless clipboard, resize, shared folders
virtualisation.virtualbox.guest.enable = true;
# Users
users.users.alice = {
isNormalUser = true;
description = "Alice";
extraGroups = [ "wheel" "networkmanager" "video" "docker" "vboxsf" ];
shell = pkgs.fish;
};
system.stateVersion = "24.11";
}
The host file says “this machine’s name, its bootloader, its virtualization-guest quirks, who its user is”. Everything else — KDE, dev tools, fish plugins — comes from the shared modules imported at the flake level.
specialArgs — passing things to modules#
By default, modules are called with { config, pkgs, lib, options, ... }. specialArgs extends that set with your own additions, available as destructurable arguments inside every module.
mkHost = hostName: extraModules:
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = {
inherit inputs;
myFlags = {
isLaptop = false;
hasNvidia = false;
};
};
# ...
};
Inside any module:
{ pkgs, inputs, myFlags, ... }:
{
imports = [ inputs.impermanence.nixosModules.impermanence ];
hardware.nvidia.enable = lib.mkIf myFlags.hasNvidia true;
}
Avoid overloading specialArgs — anything more than inputs and a small flag set belongs in a proper option defined in a module. Keep the indirection budget low.
Building and switching each host#
From the host itself#
# Classic nixos-rebuild, flake-aware:
sudo nixos-rebuild switch --flake ~/nixos-config#desktop-vbox
# Build only, inspect result:
nixos-rebuild build --flake ~/nixos-config#desktop-vbox
ls -la ./result
# Build a VM to try a change before switching:
nixos-rebuild build-vm --flake ~/nixos-config#desktop-vbox
./result/bin/run-*-vm
From another machine (remote build)#
--target-host and --build-host invert who builds and who activates:
# Build locally on Kali, activate remotely on desktop-vbox over SSH:
nixos-rebuild switch \
--flake ~/nixos-config#desktop-vbox \
--target-host root@desktop-vbox.local \
--use-remote-sudo
# Build remotely on a beefy server, activate on this machine:
nixos-rebuild switch \
--flake ~/nixos-config#portable-ssd \
--build-host user@fastbox.example.com
This is how you deploy from a laptop to a server without uploading the whole flake — nixos-rebuild copies only what’s needed over SSH.
For a non-NixOS host (Kali, WSL2-Debian)#
home-manager switch --flake ~/nixos-config#alice
No system changes — only home.nix applies. Same flake, same commit, different entry point.
inputs.follows — avoiding duplicated nixpkgs#
Every input in your flake that itself depends on nixpkgs brings its own copy. Without intervention, a flake that uses nixpkgs + home-manager + disko + impermanence can drag in four different revisions of nixpkgs.
inputs.*.inputs.nixpkgs.follows = "nixpkgs" overrides each input’s nixpkgs to match yours:
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
impermanence.url = "github:nix-community/impermanence"; # no nixpkgs dep, no follows needed
};
After these follows, every input resolves the same nixpkgs. flake.lock shows it: no nixpkgs_2, nixpkgs_3, just a single nixpkgs entry.
Adding a new host#
The reproducible recipe, the nth time you do this:
Boot the target machine (ISO, live USB, or an existing Linux host via
nixos-anywhere).Partition, mount root at
/mnt.Generate the machine-specific config:
sudo nixos-generate-config --root /mnt --dir /mnt/etc/nixos/hosts/new-host
Clone your flake to the target:
git clone git@github.com:you/nixos-config.git /mnt/etc/nixos # move the just-generated files into place mv /mnt/etc/nixos/hosts/new-host/configuration.nix /mnt/etc/nixos/hosts/new-host/configuration.generated.nix # write a short configuration.nix that imports hardware-configuration.nix and sets the bootloader, like the hosts/desktop-vbox example above
Add the host to
flake.nix:nixosConfigurations.new-host = mkHost "new-host" [ ./modules/kde.nix ./modules/dev.nix ];
Commit, then install:
sudo nixos-install --flake /mnt/etc/nixos#new-host
Reboot.
home-manager switch --flake ~/nixos-config#aliceruns automatically as part ofnixos-rebuildbecause the home-manager module is in the flake.
homeConfigurations alongside nixosConfigurations#
A common layout worth naming explicitly. Your flake exposes:
nixosConfigurations.<host>— the full system, for NixOS hosts.homeConfigurations."<user>"— the user-only config, for non-NixOS hosts.
Both pull the same ./home.nix. On a NixOS host, the NixOS module runs home-manager as part of nixos-rebuild; on Kali, the standalone CLI uses homeConfigurations.alice; both produce the same user environment.
The "alice@desktop-vbox" pattern — multiple homes per user#
If your home.nix grows enough that you want per-host variants (e.g. a more aggressive editor setup on the dev VM, a lighter one on the pentest box), expose multiple homes:
homeConfigurations = {
"alice" = ...; # default, portable
"alice@desktop-vbox" = ...; # with KDE-specific tweaks
"alice@kali" = ...; # with Kali-specific PATH adjustments
};
Then on each host: home-manager switch --flake ~/nixos-config#alice@<host>.
In practice, most people keep a single alice and use conditionals inside home.nix (lib.mkIf (builtins.getEnv "HOSTNAME" == "...") { ... }) — but the multi-entry pattern is there when you need it.
See also#
getting-started.md — the big picture, the “levels” framing.
configuration.md — the
configuration.nixanatomy and option discovery.home-manager-modes.md — the standalone vs module distinction used throughout this page.
nix-language.md — the syntax behind
mkHost = hostName: extraModules: ....tricks.md —
nixos-anywhereand remote-build workflows referenced above.