Building NixOS Installer ISOs
Scope. This skill covers the legacy
autoInstallerflow (label-based ext4, no LUKS, no impermanence, no lanzaboote) — today only ultraviolet uses it. Hosts on the btrfs-impermanence + lanzaboote stack install a different way: gnomon via the kit-partition ISO (modules/installer-iso/,.#installerIso), vermissian from its own running system (scripts/flash-vermissian.sh), stygianlibrary from gnomon (scripts/flash-stygianlibrary.sh). The option table and the agekey step below do not apply to those hosts.
Overview
The modules/installer.nix module creates bootable USB ISOs that auto-partition, format, and install a NixOS host. Each host has a thin wrapper in hosts/<name>/installer.nix setting autoInstaller.* options.
Building an ISO
nix build .#<host>InstallerIso --print-build-logs
Always use --print-build-logs — the build output contains the baked-in SSH public key:
INSTALLER SSH PUBLIC KEY for <host>:
ssh-ed25519 AAAA... <host>-installer
If the host's flake has private repo inputs (e.g. shimmer uses git+ssh://), add this public key as a deploy key on those repos before booting the USB.
Flashing to USB
sudo dd if=result/iso/*.iso of=/dev/sdX bs=4M status=progress oflag=direct
sudo eject /dev/sdX
What Happens on Boot
- NixOS minimal ISO boots, shows ASCII banner on getty
- Auto-install service starts after
network-online.target - Detects target disk (EFI boot entries > ESP heuristic > partition count > largest disk)
- Confirmation prompt via
systemd-ask-password: shows disk info, requires typingyes - Partitions and formats disk with configured labels
- If LUKS: prompts for passphrase via
systemd-ask-password - Mounts root and boot, creates extra dirs
- Clones nix-config repo
- Runs
nixos-install(prebuilt closure or--flakedepending on config) - Powers off (or reboots if
powerOff = false)
Creating an Installer for a New Host
Generate age keypair for the new host:
nix-shell -p age --run 'age-keygen'Save the private key securely (e.g.
/tmp/<host>.agekey). You'll need both the public and private components.Update
secrets/keys.nix— add or replace the host's machine-specific age public key. Each host entry has: machine key, shared SSH-derived key (age10kwzae...), and the personal key (age1yyrhr0...).Rekey secrets so they're encrypted to the new key set:
nix-shell -p ssh-to-age --run 'ssh-to-age --private-key < ~/.ssh/id_ed25519 > /tmp/rekey.agekey' nix run github:ryantm/agenix -- --rekey -i /tmp/rekey.agekeyThis decrypts all secrets with your SSH-derived age key and re-encrypts them with the updated recipient lists.
Create
hosts/<name>/installer.nix— useultraviolet(non-LUKS, swap) orvermissianas templates.Add to
flake.nixinnixosHostDefinitions:<name>-installer = { system = "x86_64-linux"; modules = [ ./hosts/<name>/installer.nix ]; };Add to
packages.x86_64-linux:<name>InstallerIso = self.nixosConfigurations.<name>-installer.config.system.build.isoImage;git addnew files (flakes only see tracked files).Commit and push — the target machine will clone this repo during install.
Build with
--print-build-logs, note the SSH public key for private repo deploy keys.After install, before first boot: copy the age private key to the installed system:
ssh <installer-ip> "sudo mkdir -p /mnt/etc/age && sudo chmod 700 /mnt/etc/age" scp /tmp/<host>.agekey <installer-ip>:/tmp/<host>.agekey ssh <installer-ip> "sudo cp /tmp/<host>.agekey /mnt/etc/age/<host>.agekey && sudo chmod 600 /mnt/etc/age/<host>.agekey"Without this, agenix cannot decrypt secrets and services that depend on secrets will fail.
Key Module Options (autoInstaller.*)
| Option | Type | Default | Description |
|---|---|---|---|
targetHost |
str | required | nixosConfiguration name to install |
prebuilt |
bool | false | Embed full closure in ISO (large, offline) vs build from flake on target (small, needs network) |
labels.efi |
str | required | EFI partition label |
labels.root |
str | required | Root filesystem label |
labels.luks |
str | "" | LUKS container partition label |
labels.swap |
str | "" | Swap partition label |
luks.enable |
bool | false | LUKS encryption |
luks.mapperName |
str | "cryptroot" | Device mapper name |
swap.enable |
bool | false | Create swap partition |
swap.size |
str | "8G" | Swap size (sgdisk format) |
extraMountDirs |
[str] | [] | Dirs to create under /mnt |
extraPostMountCommands |
[str] | [] | Shell commands after mount |
repoClonePath |
str | "/mnt/nix-config" | Where to clone nix-config |
powerOff |
bool | true | Power off vs reboot after install |
extraInitrdKernelModules |
[str] | [] | Extra initrd modules |
extraPackages |
[pkg] | [] | Extra packages in ISO |
extraBootCommands |
lines | "" | initrd stage-1 systemd service script |
Prebuilt vs Non-Prebuilt
prebuilt = false(default): ISO is ~1.3GB, builds in seconds. Target machine builds its own system from the flake. Requires network. Good for powerful machines.prebuilt = true: ISO is multi-GB, takes a long time to build (embeds entire system closure). Target installs offline in minutes. Use for machines without reliable network.
Existing Host: Switching to Labels
If a host currently uses UUIDs in hardware-configuration.nix, label partitions before rebuilding:
sudo e2label /dev/sdX2 LABEL-ROOT
sudo fatlabel /dev/sdX1 LABEL-EFI
sudo swaplabel -L LABEL-SWAP /dev/sdX3 # if swap exists
Then update hardware-configuration.nix to use /dev/disk/by-label/... and nixos-rebuild switch.
Troubleshooting
- Disk detection fails: Set
INSTALL_DISK=/dev/sdXenv var, or checklsblk -dno NAME,RM,TYPE— the heuristic only considersRM=0TYPE=disk - SSH key issues for private repos: The ISO bakes in a key at build time. Add its public component as a deploy key. The service auto-loads it via
ssh-agent - Service doesn't start: Check
systemctl status <host>-auto-installandjournalctl -u <host>-auto-install - No cachix during install: The installer module configures all cachix caches in
nix.settings. If still building from source, ensure the cache has the derivations pushed - Agenix "no readable identities": The age private key isn't at
/etc/age/<hostname>.agekeyon the installed system. Copy it before first boot (see step 10 in "Creating an Installer"). On subsequent boots, the activation script inmodules/services/age-identity.nixwill also derive an age key from the SSH host key, but the pre-generated key must be in place for the first boot