← All TILs · nixos

First steps on NixOS: the whole system is one file, and every change is a boot entry

nixos - 2026-09-25

Coming to NixOS from RHEL or Ubuntu, the instinct is to install a package, edit something under /etc, and restart a service. NixOS supports a version of that, but it isn't the model. The model is one file describing the whole machine, a command that builds that description into a new system, and a boot menu that keeps every previous build around. This entry covers those first moves, checked against the NixOS 26.05 manual, plus the one setting the manual tells you never to touch.

The configuration this entry walks through is also a working repo, dar-nixos: one commented configuration.nix you can boot as a VM on any Linux machine with Nix, plus a NixOS VM test that boots it and checks it on every push.

The machine is /etc/nixos/configuration.nix

Per the manual: "The file /etc/nixos/configuration.nix contains the current configuration of your machine." Users, services, the firewall, the bootloader, the packages: all of it is declared there, in the Nix language. A minimal change looks like this:

{ config, pkgs, ... }:
{
  environment.systemPackages = [ pkgs.htop pkgs.git ];
  services.openssh.enable = true;
}

Setting services.openssh.enable is not the same as installing the openssh package. It turns on a NixOS module, which writes the systemd unit, the config file and the firewall hole. The manual says so directly for packages in general: "Some packages require additional global configuration such as D-Bus or systemd service registration so adding them to environment.systemPackages might not be sufficient." So check whether a services.<name> option exists before reaching for systemPackages. Adding a daemon's package alone usually gets you the binary and nothing running.

Applying it: switch, test, and boot aren't interchangeable

Nothing happens when you save the file. You build it:

sudo nixos-rebuild switch

The manual describes that as: "build the new configuration, make it the default configuration for booting, and try to realise the configuration in the running system." Two siblings do only part of that, and the difference matters the first time you change something risky:

Command Activates now Becomes the boot default
nixos-rebuild switch Yes Yes
nixos-rebuild test Yes No
nixos-rebuild boot No Yes

test is the safe one for network or bootloader experiments. In the manual's words: "if (say) the configuration locks up your machine, you can just reboot to get back to a working configuration." The reboot itself is the undo button.

There's one caveat worth knowing before you rely on switch to restart everything: "This command doesn't start/stop user services automatically. nixos-rebuild only runs a daemon-reload for each user with running user services." System services get restarted. User-level units, like the rootless Quadlet containers covered elsewhere on this site, don't.

Every build is a generation, and rollback is one command

Each successful rebuild becomes a numbered generation: a symlink under /nix/var/nix/profiles/, and an entry in the boot menu. Nothing is overwritten in place. To go back while the system is running:

sudo nixos-rebuild switch --rollback

Per the manual, that's equivalent to running /nix/var/nix/profiles/system-N-link/bin/switch-to-configuration switch for the previous generation. To see what you have:

ls -l /nix/var/nix/profiles/system-*-link

If the new generation won't boot at all, pick an older one from the bootloader menu. That's the practical difference from a traditional distro, where rollback means snapshots, backups, or remembering what you changed.

flowchart LR
    EDIT["edit configuration.nix"] --> BUILD["nixos-rebuild"]
    BUILD -->|switch| G3["generation N+1<br/>active + boot default"]
    BUILD -->|test| LIVE["active until reboot,<br/>boot default unchanged"]
    BUILD -->|boot| NEXT["boot default,<br/>active after reboot"]
    G3 -->|"switch --rollback"| G2["generation N"]
    G3 -.->|"boot menu"| G1["any older generation"]

Old generations cost disk until you collect them

Keeping every build has a price: old generations pin their packages in /nix/store. The manual's command for cleaning up is nix-collect-garbage, with an important distinction. The plain form "do[es] not remove garbage collector roots, such as old system configurations. Thus they do not remove the ability to roll back." The -d form does:

sudo nix-collect-garbage -d   # deletes old generations, then collects

After that, the rollback menu is gone. To automate the safe part, the manual gives:

{
  nix.gc.automatic = true;
  nix.gc.dates = "03:15";
}

One more gotcha from the same section: if /boot fills up, clearing old profiles isn't enough on its own. You "must rebuild your system with nixos-rebuild boot or nixos-rebuild switch to update the /boot partition."

nix-env exists, and it's the other model

nix-env -iA nixos.thunderbird installs a package imperatively, into a per-user profile. The manual calls this ad hoc package management and spells out how it differs: packages installed this way are upgraded individually, whereas "running nixos-rebuild switch causes all packages to be updated to their current versions in the NixOS channel." Mixing the two means some software on the machine isn't in configuration.nix, which defeats the reason to use NixOS. For a first system, put everything in the file. For a one-off tool you need for five minutes, nix-shell -p <package> gives you a temporary shell without installing anything.

Channels are per user, and upgrades come from them

A fresh install is subscribed to the channel matching the ISO. On 26.05 that's nixos-26.05, which "only get[s] conservative bug fixes and package upgrades." To upgrade within it:

sudo nixos-rebuild switch --upgrade

That's the manual's shorthand for nix-channel --update nixos; nixos-rebuild switch. The trap is that "channels are set per user." Running nix-channel --add without sudo changes your user's channel, not the one configuration.nix is built from. The upgrade appears to do nothing, because the system never saw it.

The one line you should leave alone: system.stateVersion

The generated config ends with something like system.stateVersion = "26.05";. It looks like a version number to bump on each upgrade. It isn't. From the option's own definition in nixpkgs:

Most users should never change this value after the initial install, for any reason, even if you've upgraded your system to a new NixOS release.

It records which NixOS release first created the machine's data, so modules for things like databases keep defaults compatible with data that can't migrate itself. The same description states the misconception outright: "This value does not affect the Nixpkgs version your packages and OS are pulled from, so changing it will not upgrade your system." The upgrade comes from the channel. Leave stateVersion at whatever the installer wrote.

Flakes: you'll see them everywhere, and they're still experimental

Most NixOS guides online use flake.nix rather than configuration.nix alone. The installer supports it (nixos-generate-config --flake), but in the Nix 2.34 reference manual both flakes and the new nix subcommands are still listed as experimental features you have to opt into. You can learn NixOS without them. Everything above works with the plain configuration.nix and channels, and flakes make more sense once that model is familiar.

How this compares to tools already on this site

This is the same declarative idea as Terraform/OpenTofu, applied to one machine's whole OS instead of cloud resources. It shares one property with Terraform: there's no agent watching for drift. Nothing reapplies configuration.nix until you run nixos-rebuild. The difference is that the previous state is still sitting on the boot menu, which Terraform's state file can't give you.

Created 2026-09-25T20:02:38+02:00 · Edit