Installing NixOS
Installing NixOS
Nix the package manager is not NixOS. NixOS is a Linux distribution whose kernel, initrd, systemd units, users, and /etc are one derivation. The boring default is: install once on a disposable VM, generate hardware config, write a tiny configuration.nix, and treat nixos-rebuild switch as the only way the machine changes.
Do not practise on the only disk that holds your photographs.
Mental model
The installer ISO is a live NixOS. You boot it, partition a disk, generate a hardware snapshot, write a system expression, and run nixos-install (plain, or --flake .#host once a flake exists). That produces generation 1. From then on you edit code and rebuild. You do not apt install. You do not systemctl enable by hand.
The 26.05 live ISO already has flakes. You do not need to nix-env -iA nixos.git just to clone the flake. nixos-install --flake /mnt/etc/nixos#desk-vm is the same install as the classic /mnt/etc/nixos/configuration.nix path, with a lockfile.
| File | Role |
|---|---|
hardware-configuration.nix |
Disk UUIDs, file systems, kernel modules. Generated. Rarely edited. |
configuration.nix |
Hostname, users, packages, services, bootloader. You write this. |
/nix/var/nix/profiles/system |
Pointer at the current generation |
| Bootloader menu | Every previous generation you can roll back to |
installer ISO
│
▼
partition + mount → /mnt and /mnt/boot
│
▼
nixos-generate-config --root /mnt
│
▼
edit /mnt/etc/nixos/configuration.nix
│
▼
nixos-install → reboot → generation 1
This book targets NixOS 26.05+. After install, nixos-version should agree.
Worked examples
Case 1: Disposable VM, not a daily driver
Create a UEFI VM:
| Resource | Starting point |
|---|---|
| Firmware | UEFI |
| RAM | 4 GB (8 GB is more comfortable) |
| Disk | 40 GB |
| CPU | 2 cores |
Download the graphical or minimal NixOS 26.05 ISO from the NixOS download page. Attach it. Boot. You are in a live environment with a root shell (minimal ISO) or a desktop (graphical ISO).
Check firmware:
ls /sys/firmware/efiIf that directory exists, you are in UEFI mode. Match the VM firmware to the bootloader you will enable (systemd-boot for UEFI).
Case 2: Partition, mount, generate config
On a single empty virtual disk /dev/vda (names vary: vda, sda, nvme0n1):
lsblkA boring GPT layout for UEFI:
| Partition | Size | Type | Mount |
|---|---|---|---|
/dev/vda1 |
1 GB | EFI System | /mnt/boot |
/dev/vda2 |
rest | Linux filesystem | /mnt |
Using parted and mkfs by hand is acceptable for this first machine. Disko (later in the book) replaces this for fleets. For one VM:
parted /dev/vda -- mklabel gpt
parted /dev/vda -- mkpart ESP fat32 1MiB 1GiB
parted /dev/vda -- set 1 esp on
parted /dev/vda -- mkpart primary ext4 1GiB 100%
mkfs.fat -F32 /dev/vda1
mkfs.ext4 /dev/vda2
mount /dev/vda2 /mnt
mkdir -p /mnt/boot
mount /dev/vda1 /mnt/bootGenerate the hardware snapshot:
nixos-generate-config --root /mnt
ls /mnt/etc/nixos/Output:
configuration.nix
hardware-configuration.nix
Open hardware-configuration.nix. You should see the file-system UUIDs for / and /boot. Do not invent UUIDs. Leave this file as generated.
Case 3: A tiny first configuration.nix
Replace the generated configuration.nix with something you can read in one screen. Save as /mnt/etc/nixos/configuration.nix:
# configuration.nix
{ config, pkgs, ... }:
{
imports = [ ./hardware-configuration.nix ];
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.loader.systemd-boot.configurationLimit = 8;
networking.hostName = "desk-vm";
time.timeZone = "UTC";
i18n.defaultLocale = "en_US.UTF-8";
users.users.deskadmin = {
isNormalUser = true;
extraGroups = [ "wheel" ];
# Lab only. Hashed at eval; the *hash* still lands in the store.
# After first login: passwd, then delete this option.
initialPassword = "changeme";
};
environment.systemPackages = with pkgs; [
git
vim
curl
];
services.openssh.enable = true;
system.stateVersion = "26.05";
}stateVersion is not “the release I want to be on.” It is “the release whose defaults this machine was born with.” Leave it at the release you installed. Changing it later without reading the release notes is how databases and state dirs break.
configurationLimit = 8 keeps the ESP from filling with old kernels. The first-machine chapter adds GC; this is the bootloader side of the same hygiene.
Set a real password after first login (passwd). Then remove initialPassword. That option hashes at eval time; the hash is still world-readable in /nix/store. Production uses hashedPasswordFile (sops) or users.mutableUsers = true plus a one-shot passwd. hashedPassword = "…" in the module is the same store leak as initialPassword, just pre-hashed.
Install from the generated files (no flake yet):
nixos-install --no-root-passwd
nixos-enter --root /mnt -c 'passwd deskadmin'
reboot--no-root-passwd skips the interactive root prompt when you already declared a wheel user. nixos-enter sets the password inside the installed root, not in the live ISO. nixos-install without that flag still works; it will ask for a root password.
Flake install (26.05 default once git exists). After nixos-generate-config, drop a flake next to the two Nix files. Save as /mnt/etc/nixos/flake.nix:
# flake.nix
{
description = "Desk VM";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }: {
nixosConfigurations.desk-vm = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./configuration.nix ];
};
};
}nixos-install --flake /mnt/etc/nixos#desk-vm --no-root-passwdThe #desk-vm name is the nixosConfigurations attr, not a guess. --root defaults to /mnt. Copy flake.lock into git on first boot; the next rebuilds use --flake .#desk-vm. Do not nixos-install (no --flake) after you moved the machine to a flake — that builds the old channel configuration.nix.
Log in as deskadmin. Confirm:
nixos-version
hostnameOutput:
26.05.9440.6aefcda (Yarara)
desk-vm
Case 4: The first rebuild on the installed system
You are now on the machine. Edit /etc/nixos/configuration.nix (or the flake in git). Add ripgrep to environment.systemPackages and activate. If you installed with --flake, keep using it:
sudo nixos-rebuild switch --flake /etc/nixos#desk-vmWithout --flake, this rebuilds the installer-era channel config. After git lives elsewhere, --flake .#desk-vm from the clone.
Output:
building the system configuration...
updating GRUB/systemd-boot...
activating the configuration...
setting up /etc...
reloading user units...
which rg
rg --versionrg is on PATH. It came from the new generation, not from a manual download.
List generations:
sudo nix-env --list-generations --profile /nix/var/nix/profiles/systemYou should see generation 1 (install) and generation 2 (ripgrep). The bootloader menu lists both.
Case 5: Rollback without a rescue disk
Break the machine on purpose in the VM. In configuration.nix, set a nonsense timezone or remove boot.loader.systemd-boot.enable — something that still builds but is wrong — or, safer, add a package, switch, then:
sudo nixos-rebuild switch --rollbackOutput:
switching to generation 1
activating the configuration...
rg is gone. The bootloader default is generation 1 again. If a rebuild leaves the system unbootable, pick the previous generation in systemd-boot (hold Space at firmware, then select the older NixOS entry).
The trap
The trap is mutating the live system after install. passwd for the first login is fine. useradd, systemctl enable nginx, curl | sh into /usr are not. Those changes vanish on the next rebuild, or worse, they survive in /etc as unmanaged files that fight the next activation.
A second trap is editing hardware-configuration.nix to “clean it up.” The file is ugly because hardware is ugly. If you replace UUIDs with /dev/vda2, the next disk reorder will fail to boot.
A third trap is treating stateVersion as an upgrade knob. Upgrade by changing the flake input (or channel) to a new release and reading the release notes. Keep stateVersion at the original value unless a release note tells you to bump a specific option.
A fourth trap is shipping initialPassword (or hashedPassword = "…") on a host that will ever be copied. nix path-info -r /run/current-system includes the hash. Rotate, then hashedPasswordFile.
The boring rule
- Practise on a VM. UEFI. One ESP, one root.
systemd-boot+configurationLimit. nixos-generate-configownshardware-configuration.nix. You ownconfiguration.nix(andflake.nix).nixos-install --flake /mnt/etc/nixos#hostonce a flake exists. Same attr name asnixosConfigurations.system.stateVersionis the birth release. Leave it.- After install, the only mutation path is
nixos-rebuild switch --flake(ortest, orboot). - Know
--rollbackand the bootloader menu before you need them. initialPasswordis lab-only. Delete it afterpasswd.
Try this
- After Case 4, run
sudo nixos-rebuild testwith a hostname change. Confirmhostnamechanged, reboot, and confirm it reverted (test does not update the bootloader default). - Run
sudo nixos-rebuild dry-activateand read which units would restart. No change should be applied. - Add
cowsaytoenvironment.systemPackages, switch, runcowsay desk, then rollback and confirmcowsayis missing. - Open
hardware-configuration.nixand write down the UUID of/. Compare withlsblk -f. They must match. - Install a second VM with
--flake /mnt/etc/nixos#desk-vm. Confirmnixos-versionstill says 26.05 andls /etc/nixos/flake.lockexists.