↓ Skip to main content

NixOS (2): Core Concepts — Declarative Configuration, Flakes and Rollback

·1216 words·6 mins
Jin Li
Author
Jin Li
Fate lies within the lightcone.
NixOS Series - This article is part of a series.
Part 2: This Article

Motivation
#

The previous post explained why I returned to NixOS. Names such as Nix, nixpkgs, flakes, derivations and generations come up frequently. They refer to package management, configuration inputs, builds and system versions.

This post introduces the common concepts and uses configuration examples and commands to explain their purpose. You do not need to learn the whole Nix language first; the examples below provide a starting point.

Declarative Configuration
#

Manual and declarative configuration
#

Setting up a service on Fedora usually involves installing its package, editing configuration files and enabling the service. If other computers need access, the firewall also needs checking.

This approach requires recording the installation and configuration steps separately. When repeating the setup months later on another computer, an incomplete record can lead to a missing file or command. A different software version may also have different defaults.

NixOS uses declarative configuration: you write down the software, services and settings you want, and the system prepares the required packages and files.

For example, to enable Tailscale in a NixOS module:

1
services.tailscale.enable = true;

The corresponding module handles the package and systemd service configuration, so there is no separate package installation or service file to write. Initial account authentication still needs to be completed.

Another example allows TCP port 22 through the firewall:

1
networking.firewall.allowedTCPPorts = [ 22 ];

NixOS generates rules for the configured firewall backend. This setting only allows the port; the SSH server still needs to be enabled separately.

Many system settings can therefore be kept in files and tracked in Git. On another computer, the common parts can be reused while hardware differences are handled separately. User documents and database contents are not part of those declarations and still need to be copied or restored.

Nix and /nix/store
#

Nix is a package manager, NixOS is the Linux distribution that uses it to manage the system, and the configuration language is also called Nix. When reading documentation, use the context to distinguish the package manager from the language.

You will often see paths like this:

1
/nix/store/<hash>-<name>

Packages are normally stored here, with a hash followed by a name. An upgrade adds a new store path instead of overwriting the old package. Old and new versions can coexist, and different programs can use the dependency versions they require.

A package already in the store can be reused. If a trusted binary cache has the required output, Nix can download it instead of compiling it locally.

The hash should not simply be treated as a fingerprint of the final file contents. A derivation describes a build recipe and its inputs; an ordinary derivation’s output path is usually calculated from that information, which is called input addressing. An identical path alone therefore does not prove that two builds produced identical bytes. Nix also supports content-addressed objects; the store path specification explains the distinction. For the rollback examples, the useful starting point is that old and new packages can be kept at separate paths.

A complete NixOS system also has a store path. The system and all its dependencies together are called its closure. Keeping those paths available makes it possible to use the old system again.

nixpkgs
#

nixpkgs contains package definitions and NixOS modules. It describes where a package’s sources come from, which dependencies it needs and how to build it. It also provides the modules that expose system and service options.

The packages and options available in your configuration depend on the nixpkgs version you use. For example, an “option does not exist” error after following a tutorial calls for checking both the spelling and whether the tutorial uses the same nixpkgs version as your locked input.

Flakes and flake.lock
#

Flake inputs and outputs
#

When a configuration uses nixpkgs, Home Manager and other modules, it needs to specify where those dependencies come from. A flake calls them inputs. The results it provides are its outputs.

For system configuration, the outputs usually include a NixOS configuration for each computer. Suppose there is an output called laptop; it can be applied with:

1
sudo nixos-rebuild switch --flake /path/to/nixos-config#laptop

/path/to/nixos-config is the repository directory. The name after # selects an output and does not have to match the computer’s network hostname. Replace these example values with your own.

One repository can provide several outputs, so each computer does not need a separate copy of the entire configuration. The next post shows how to arrange this.

Locking inputs with flake.lock
#

Suppose the flake follows a particular nixpkgs branch. That branch keeps moving, so the version available today may differ from the one available next month. A branch name alone does not tell us what the previous build used.

flake.lock records the exact input versions and hashes. Reusing that lock file keeps the selected inputs rather than automatically following the latest branch contents. It should therefore be committed to Git too.

To update dependencies deliberately:

1
2
nix flake update
git diff -- flake.lock

Look at which inputs changed before building. The lock file helps reuse the same inputs, but it does not back up remote sources or application data.

Building, Activation and Rollback
#

Editing a configuration file is only part of applying a change. The rest broadly follows these steps:

  1. Evaluation reads the Nix files, combines modules and options, and produces build instructions. Syntax errors and some option conflicts are reported here.
  2. Building prepares the required software and files, reusing existing outputs or downloading them from caches where possible.
  3. Activation applies the configuration to the running system, including managed files and affected services.
  4. Checking after a reboot is needed for a new kernel or boot-related configuration, to confirm the system actually starts correctly.

Checking, building and activating are separate operations. The following commands perform them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Check the flake without building its declared checks
nix flake check --no-build

# Build the target system without activating it
nixos-rebuild build --flake .#laptop

# Activate temporarily without changing the boot default
sudo nixos-rebuild test --flake .#laptop

# Activate and make it the boot default
sudo nixos-rebuild switch --flake .#laptop

Without --no-build, nix flake check also builds the flake’s checks; see the command reference. These checks can find many problems, but you still need to try the service or feature itself.

The NixOS system profile retains generations, which you can list with:

1
sudo nixos-rebuild list-generations

If a configuration causes problems after switch, check that the previous generation is the one you want, then roll back:

1
sudo nixos-rebuild switch --rollback

If the new system cannot boot, select a retained older generation in the boot menu. Returning to an older kernel requires booting that kernel.

System rollback is not a snapshot restoration of the whole disk. If a service upgrade has changed its database, the old software may not be able to use the updated data. Activation also involves several operations, so a failure can leave some changes applied.

Garbage collection can reclaim store paths belonging to old generations. Once those paths are gone, they are no longer available for rollback. Post 5 works through temporary activation, recovery, reboots and cleanup using a small example.

With these concepts in place, we can look at organizing several computers in one flake.

NixOS Series - This article is part of a series.
Part 2: This Article