NixOS and Declarative Machines
LESSON
NixOS and Declarative Machines
By the end of this lesson, you will be able to...
explain how a NixOS configuration change becomes a new system generation and a running machine;
choose between build, test, boot, switch, and rollback according to the risk of one workstation change;
separate declaratively managed system state from runtime behavior, secrets, mutable data, and recovery evidence.
Idea in one sentence: NixOS makes a declared system configuration reproducible by building generations from it, but the declaration is useful only when you still observe the running machine and name the state it does not own.
Core Insight
Maya's Arch ownership record has made her workstation understandable, but rebuilding it still means replaying decisions: install packages, restore a service unit, copy configuration, then test whether the result works. She wants the next laptop to begin from a machine description instead of a checklist.
The tempting model is: “Put every file into configuration.nix, run one command, and the laptop is completely reproduced.” It works for a narrow class of system choices: packages, users, services, and options that NixOS modules know how to configure.
It breaks when a service has mutable data, an application needs a secret, hardware differs, a user creates state after login, or a declared service starts successfully but behaves incorrectly. A configuration file cannot automatically turn unknown runtime state into safe, reproducible state.
The stronger model is:
A NixOS configuration is a desired-state specification for the parts you choose to manage. NixOS evaluates and builds that specification into a system generation, activates it in a running machine, and retains generations for recovery until they are removed.
NixOS documents this model directly: the system configuration describes the intended system, and nixos-rebuild builds and realizes it. The default configuration file is commonly /etc/nixos/configuration.nix, but a real setup may split it into modules or use a flake. NixOS Manual
The trade-off is explicit: declaration reduces unrecorded configuration drift and gives a structured rollback path, but it costs learning a configuration model, evaluating changes before use, and keeping its boundary with mutable state honest.
The Promise We Need to Keep
Maya's first NixOS goal is deliberately small. Her laptop should contain Git and an editor, and it should enable an SSH service that she can verify and later reverse. A simplified configuration fragment looks like this:
{ pkgs, ... }:
{
environment.systemPackages = [
pkgs.git
pkgs.neovim
];
services.openssh.enable = true;
}
The snippet is a declaration, not a shell transcript. It says which packages and service state are desired; it does not show every command that will be run during activation, and it does not by itself prove the SSH service's authentication policy or network exposure is appropriate for Maya's laptop.
Read the declaration as a design boundary:
| Declared item | NixOS can derive or manage | Maya must still decide and verify |
|---|---|---|
pkgs.git, pkgs.neovim |
A consistent system package set for the chosen configuration | Whether these tools meet the work need and where user configuration lives |
services.openssh.enable |
System service configuration and its activation as a system service | Who may connect, which network boundary is acceptable, and whether the service behaves as expected |
| A configuration module | The merged option values and a system generation | Which machine-specific facts, secrets, and data should stay outside the module |
| A generation | A bootable/configurable system result while retained | Backups for user data and a recovery plan if hardware or a secret is missing |
This corrects a subtle misconception from the Arch lesson. Declarative configuration does not remove ownership; it moves some ownership from a remembered sequence of commands into a versioned description and its review process.
From a Declaration to a Running Machine
The invisible steps become easier to reason about when we draw them:
edited NixOS configuration
-> evaluate options and dependencies
-> build a candidate system generation
-> choose how to activate it
-> system services and runtime state change
-> verify the real behavior and retain evidence
The first three steps answer, “Can this description produce a coherent candidate?” The last two answer, “Did this particular laptop now do the thing we wanted?” They are different questions.
NixOS provides several rebuild modes for different points in this path:
| Command | What it changes | Good fit |
|---|---|---|
nixos-rebuild build |
Builds the configuration but does not activate it | Check whether the description can build before touching the current system |
nixos-rebuild test |
Activates the candidate now, but does not make it the default boot configuration | A reversible trial when a reboot should return to the prior default |
nixos-rebuild switch |
Activates the candidate now and makes it the boot default | A reviewed change whose runtime effect you are ready to verify |
nixos-rebuild boot |
Makes the candidate the boot default without switching the current runtime | A change intentionally deferred until the next reboot |
nixos-rebuild switch --rollback |
Switches the running system back to the previous configuration | Recover from a bad recent generation after identifying the failed change |
These modes are official behavior, not names to memorize. In particular, the manual says that test activates a configuration without making it the boot default, while switch activates it and makes it the default; it also documents rollback and boot-menu recovery. NixOS Manual: changing configuration
Worked Change: Test Before You Make It the Default
Maya adds the packages and SSH service fragment to a small module. Her initial temptation is to run switch immediately because the text is short. Instead, she makes a change plan.
Goal: Git and Neovim are on the system path; sshd is available only under the intended access policy.
Expected declarative diff: two packages and an enabled system service.
Runtime evidence: command paths resolve; systemctl reports the service state; a deliberately permitted connection behaves as expected.
Recovery: reboot after test, or roll back the previous generation after a switch.
Non-goal: copy user SSH keys, editor preferences, or secrets into the system configuration without a separate boundary decision.
First, build the candidate:
sudo nixos-rebuild build
If evaluation or building fails, Maya has learned something about the declaration without changing the active system. A successful build is useful evidence, but it does not prove that the service will accept the intended connection or that a firewall policy is right.
Next, on a machine where testing the system SSH service is safe, she tries the candidate without making it the next boot default:
sudo nixos-rebuild test
systemctl status sshd.service
command -v git
command -v nvim
The exact unit name and access checks are details to verify against the installed module and the local policy. The point is the trace:
| Step | State Maya observes | Inference |
|---|---|---|
Before test |
Current generation is the boot default | A reboot is a simple escape from a bad trial generation |
| Build succeeds | Nix can produce the candidate from the declaration | Syntax and option evaluation were sufficient for a build |
test activates |
Current runtime receives the candidate's activation | Runtime services may be restarted or changed now |
| Status and an explicit connection check | Process state and user-visible behavior | The declared change actually meets the goal, or the next failure signal is visible |
| Reboot after a failed trial | Boot returns to prior default generation | The trial was not silently promoted |
When the acceptance checks are satisfactory, sudo nixos-rebuild switch promotes the same kind of declared result to both current runtime and boot default. Record the configuration diff, the generation or time of the change, and the acceptance evidence in the ownership record from lesson 005.
There is an important boundary: the NixOS manual warns that nixos-rebuild does not automatically start or stop user services; it runs daemon-reload for users with running user services. A system declaration therefore does not remove the need to inspect the actual service scope and runtime behavior. NixOS Manual: changing configuration
Declarative Does Not Mean “All State Is Reproducible”
The configuration can specify packages and many system services, but these common categories need their own policy:
| State | Why a machine description is not enough | Useful boundary |
|---|---|---|
| Secrets | Their values must not become casually shareable configuration | A secret-management and recovery method with access controls |
| User documents and databases | They change because work happens, not because configuration is evaluated | Backup, synchronization, and restore evidence |
| Caches and build outputs | They are often disposable and machine-local | Treat as derived state unless a specific workflow says otherwise |
| Hardware facts | Device paths, firmware, disks, and displays differ between machines | A host-specific module plus a validation step on the target hardware |
| Application preferences | Some are declarative; others are written at runtime | Classify individually instead of assuming every dotfile belongs in the system config |
This is not a limitation unique to NixOS. It is the same distinction the track has built from files, packages, and services: desired configuration, private values, and mutable runtime state have different lifecycles. The next lesson makes that classification explicit for dotfiles and personal platforms.
Generations Are a Recovery Tool, Not a Backup Plan
NixOS keeps system configurations as generations while their roots are retained. If a new configuration does not boot, the boot menu can select an earlier configuration; if the running configuration is bad, nixos-rebuild switch --rollback returns to the previous generation. NixOS Manual: rollback
That is powerful, but narrow. A rollback can restore a previous system configuration; it does not restore a deleted project file, un-rotate a compromised secret, reverse an external API call, or preserve generations that have already been garbage-collected. Garbage collection is therefore a recovery-boundary decision: removing old roots can also remove the ability to roll back to those generations. NixOS Manual: necessary system state
The signal to watch is not merely “a generation exists.” Ask: Does this generation contain the configuration I need, and are the data, secrets, hardware facts, and access methods needed to use it still available?
Imperative and Declarative Changes Under Constraints
Neither model wins by identity. Compare them under the job.
| Constraint | Explicit imperative ownership can fit when… | Declarative NixOS ownership can fit when… |
|---|---|---|
| One temporary experiment | The state is deliberately short-lived and recorded as such | The experiment should be repeatable across machines or survive a rebuild |
| Rebuild a personal workstation | The checklist is small and each decision is easy to replay | Packages, services, users, and policy should be regenerated from one reviewed description |
| Diagnose runtime failure | You need to inspect the live command, package, unit, and logs directly | You also need to compare the declaration with the active generation and live service |
| Undo a bad change | You can reverse the known mutation safely | A retained generation provides a tested configuration-level rollback path |
The trade-off is not “manual freedom versus automation.” NixOS gains consistency, reviewability, and generations; it costs conceptual overhead and demands discipline about non-declarative state. It is a good fit when the recovery and repeatability benefit is larger than that cost.
Check Your Understanding
Check: A NixOS configuration builds successfully, but the enabled service fails its real acceptance check. What has the build established, and what remains unknown?
Think first, then reveal.
Answer: The declaration evaluated and produced a candidate system generation. It has not proved the service's runtime policy, network reachability, credentials, data access, or user-visible behavior. Inspect the live unit and logs, then revise the declaration or its surrounding state boundary.
Check: Maya used nixos-rebuild test, then discovers that the candidate breaks a local workflow. What is her simplest immediate recovery, assuming the previous boot default was healthy?
Think first, then reveal.
Answer: Reboot. test activates the candidate without making it the boot default, so the machine returns to the prior default generation. She should also record the observed failure before changing the declaration again.
Practice: Design a Declarative Change Plan
Choose a small system change: add one package, enable one known service, or create one user account. Write a six-line plan before applying it:
- desired declaration and the capability it should provide;
- state that remains outside the declaration;
- lowest-risk rebuild mode that can test the change;
- runtime acceptance check, not only a successful build;
- generation rollback or reboot path;
- evidence you will preserve if the change fails.
A good answer names a change NixOS can actually manage, uses build or test when that matches the risk, and does not claim that a generation backs up user data or secrets.
Resources
- [DOC] NixOS Manual — Focus: read the declarative configuration model, rebuild modes, configuration modules, and recovery behavior as one operational system.
- [DOC] NixOS configuration options — Focus: verify real module option names and semantics before adding them to a machine description.
- [DOC] Nix & NixOS overview — Focus: connect package management and system configuration to the broader declarative model without treating either as magic.
Key Takeaways
- A NixOS declaration describes the desired managed system; it is not a complete snapshot of all machine and user state.
- Build, test, boot, switch, and rollback are different risk controls for moving from a declaration to a running machine.
- A successful build proves that a candidate can be produced, not that services, secrets, data, hardware, or user-facing behavior are correct.
- Generations improve configuration recovery while retained, but they do not replace backups or a boundary for mutable state.
- Declarative ownership still requires runtime inspection; it changes where decisions are recorded, not whether the machine must be understood.
← Back to Linux Workstations: Ownership, Reproducibility, and Repair