Table of Contents
Chapter 10: Declarative Configuration
Imperative vs. Declarative
On a traditional Linux distribution, you configure your system imperatively -- you run commands that change state:
sudo apt install firefox
sudo systemctl enable bluetooth
sudo useradd -m nimmo
Each command mutates the system. If you want to replicate this setup on another machine, you need to remember (or script) every command you ran, in the right order, accounting for whatever state already exists.
NixOS takes a fundamentally different approach. You declare the state you want, and NixOS figures out how to get there:
{
programs.firefox.enable = true;
hardware.bluetooth.enable = true;
users.users.nimmo.isNormalUser = true;
}
There are no commands to run. You describe the desired end state, and nixos-rebuild switch makes it so. If Firefox is already installed, nothing happens. If it is not, it gets installed. If you remove the line and rebuild, Firefox gets removed.
What This Means in Practice
Your entire system is described in files
Every aspect of your NixOS system -- packages, services, users, kernel parameters, boot configuration, firewall rules -- is described in .nix files. There is no hidden state, no forgotten apt install from six months ago. If it is not in your configuration, it is not on your system.
Configuration is reproducible
Give someone your flake.nix and flake.lock and they can build the exact same system. Not a similar system -- the exact same system, down to the specific versions of every package.
Changes are atomic
When you run nixos-rebuild switch, NixOS builds the entire new system configuration and switches to it atomically. If the build fails, your running system is untouched. If the switch fails, you can reboot into the previous generation from the boot menu.
You can roll back
Every successful nixos-rebuild switch creates a new "generation." Previous generations remain on disk. You can switch back to any of them:
# List generations
sudo nix-env --list-generations --profile /nix/var/nix/profiles/system
# Boot menu also shows previous generations
This is why your garbage-collection.nix keeps generations for 3 days -- they are your safety net.
The NixOS Module System
The mechanism that makes declarative configuration work is the NixOS module system. Your configuration is not one big file; it is composed of many modules that each declare a piece of the system, and NixOS merges them all together.
When you write:
# In modules/hardware/bluetooth.nix
hardware.bluetooth.enable = true;
# In modules/common/system.nix
environment.systemPackages = with pkgs; [ btop htop ];
# In modules/profiles/gaming.nix (via home-manager)
home-manager.users.nimmo.home.packages = with pkgs; [ discord mangohud ];
NixOS takes every module you import, collects all their option assignments, merges them according to well-defined rules, and produces a single complete system specification. Then it builds that specification into a working system.
Merging rules
The module system handles the common cases intelligently:
-
Lists get concatenated. If two modules both set
environment.systemPackages, both lists of packages are combined. Your system.nix packages and bluetooth.nix packages all end up installed. -
Booleans and strings must not conflict. If two modules set the same boolean to different values, NixOS raises an error (unless one uses
mkDefaultormkForceto set priority). -
Attrsets get merged recursively. Two modules can both set attributes under
services.pipewireand they will be combined into one attrset.
This is why your configuration can be split across many files without conflict. Each file declares its piece of the system and NixOS combines them all.
The Nix Store
All software in NixOS lives in /nix/store/. Each package gets a unique path based on a cryptographic hash of everything that went into building it:
/nix/store/abc123...-firefox-134.0/
/nix/store/def456...-git-2.47.0/
This means:
- Multiple versions of the same package can coexist (different hash)
- Packages are immutable once built (the store is read-only)
- You can always trace exactly what inputs produced a given package
Your system's /usr/bin/, /usr/lib/, etc. are not used. Instead, NixOS creates a system profile that symlinks to the correct store paths.
Generations
Each time you nixos-rebuild switch, NixOS creates a new generation. A generation is a pointer to a specific system configuration in the Nix store. Your boot menu shows available generations so you can boot into any of them.
Your configuration manages generations through modules/maintenance/garbage-collection.nix, which:
- Deletes generations older than 3 days
- Caps total generations at 20 (deletes oldest first if over cap)
- Runs daily on a timer with a randomized 1-hour delay
How a Rebuild Works
When you run sudo nixos-rebuild switch --flake .#electra, here is what happens:
-
Evaluation: Nix reads
flake.nix, finds theelectraconfiguration, and evaluates all the modules. This resolves every import, merges all attrsets, and produces a complete system specification. -
Building: Nix builds every package and configuration file needed. If a binary is available from a cache (like
cache.nixos.org), it downloads it. Otherwise, it builds from source. -
Activation: The new system profile is created and the switch script runs. Services are restarted, symlinks are updated, and the new configuration becomes active.
-
Boot entry: The new generation is added to the boot menu so it persists across reboots.
If step 2 fails, nothing changes. If step 3 fails, you can reboot into the previous generation. This is fundamentally safer than apt upgrade where a failure can leave your system in a partially-updated state.
Why This Matters for You
Understanding the declarative model changes how you think about system administration:
- "How do I install X?" becomes "Where do I declare X?"
- "Something broke after an update" becomes "Which generation was working? Boot into it."
- "How do I set up my new laptop?" becomes "Point it at my flake and rebuild."
- "What is different between my two machines?" becomes "diff the host configurations."
Your configuration repository is the single source of truth for your entire system. Treat it with care, keep it in git, and you will always be able to reconstruct your system from scratch.