NixOS and Declarative Machines

LESSON

Linux Workstations: Ownership, Reproducibility, and Repair

006 25 min beginner

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:

  1. desired declaration and the capability it should provide;
  2. state that remains outside the declaration;
  3. lowest-risk rebuild mode that can test the change;
  4. runtime acceptance check, not only a successful build;
  5. generation rollback or reboot path;
  6. 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

Key Takeaways

PREVIOUS Arch as Ownership Practice NEXT Dotfiles, Secrets, and Personal Platforms