Contents
last updated 2024-01-06 for nix 2.13 - 2.18, nixos 23.05 - 23.11
NixOS Modules
Continuing from the introduction, this is a guide to NixOS and how it builds on the concepts introduced by Nix. In particular we'll talk about how to use its configuration module system to customize NixOS systems as well as Home Manager user environments.
If you have any questions, complaints, or suggestions, contact us!
What is NixOS?
NixOS is a linux distribution that's built with Nix and configured entirely in the Nix language with a declarative configuration library in Nixpkgs called the module system.
Additionally, unlike a normal distribution, all system applications and libraries are managed entirely in the Nix store, too. The usual global locations look like this on NixOS!
$ cd /
$ tree bin usr sbin lib lib64
bin
└── sh -> /nix/store/...
usr
└── bin
└── env -> /nix/store/...
sbin [error opening dir]
lib [error opening dir]
lib64 [error opening dir]
Since Nix isn't allowed to affect the filesystem outside the store directly, NixOS functions as a declarative "system generator", producing a script inside the store that, when run, idempotently transmutes the currently running system configuration into the new one.
nixos-rebuild
The way NixOS "escapes the store" to modify system state is through the
script nixos-rebuild
. You can
read the code to see what it does, but essentially, if you run
nixos-rebuild switch
, this is what happens:
# build the system described by the configuration
#
# <nixpkgs/nixos> evaluates the configuration at $NIXOS_CONFIG, or
# at <nixos-config>, which is by default `/etc/nixos/configuration.nix`
system=`nix build --impure --no-link --print-out-paths \
--expr '(import <nixpkgs/nixos> {}).config.system.build.toplevel'`
# update the system's profile with a new generation that points to the new system object
nix build --profile /nix/var/nix/profiles/system $system
# pass off execution to a script inside the new system object, allowing it to
# 1. update the bootloader
# 2. stop/start/reload/restart systemd units
# 3. mount/unmount/remount filesystems
# 4. run $system/activate, which is a series of idempotent "activation scripts"
# that make sure the running system conforms to the new configuration
$system/bin/switch-to-configuration switch
That last step is important: this is the only way a NixOS system mutates
system state outside the store! Once in
$system/bin/switch-to-configuration
for every configuration
switch, and once in $system/activate
for every boot.
The other subcommands of nixos-rebuild
do small variations on
the same thing:
# nixos-rebuild...
# `
# | subcommand | builds what? | updates profile? | switch-to-configuration? |
# +--------------+--------------------+------------------+--------------------------+
# | switch | toplevel | yes | yes |
# | boot | toplevel | yes | yes |
# | test | toplevel | no | yes |
# | dry-activate | toplevel | no | yes |
# | build | toplevel | no | no |
# | dry-build | toplevel --dry-run | no | no |
# | build-vm | vm | no | no |
# | build-vm-wi..| vmWithBootloader | no | no |
# th-bootloader
# `
# switch-to-configuration...
# `
# | subcommand | installs bootloader? | activate? |
# +--------------+----------------------+------------------+
# | switch | yes | yes |
# | boot | yes | no |
# | test | no | yes |
# | dry-activate | no | no, dry-activate |
# `
Switching to a new configuration is roughly equivalent to installing the system for the first time. Every deviation from the "default" NixOS system is recorded in your declarative configuration. Normal distributions can suffer from technical debt from the weight of forgotten ad hoc modifications to system configuration over long periods of time, but NixOS's declarative system allows you to largely avoid this.
However, keep in mind NixOS is not a totally immutable system. Most of the root filesystem can exist on a tmpfs or get wiped on reboot and NixOS will function normally, but you should take care to understand what kind of important state is persisted across system generations:
# for booting and activating the system
# nixos can boot if you delete *everything but these*, it just won't remember any state!
/boot/
/nix/
# for identifying a machine across reboots
/etc/machine-id
/etc/ssh/ssh_host_*
# for persisting ad hoc users/groups, unless all are declarative
/etc/{passwd,shadow,group,subuid,subgid}
# user state
/root/
/home/
# application state
/var/{db,log,lib,spool}/
The Module System
So, to modify the behavior of $system/bin/switch-to-configuration
and enter a new system state, we need to know how to modify the configuration
that <nixpkgs/nixos>
evaluates.
A NixOS configuration is merged together from a collection of modules. Modules are specially structured attribute sets that can
- import other modules,
- declare a set of configuration options that other modules can define,
- and define or redefined the aforementioned options to have a specific value.
They look like this:
{
config, # the final configuration after merging all option definitions
options, # the set of all declared options
lib, # the nixpkgs standard library
pkgs, # the nixpkgs package set
modulesPath, # <nixpkgs/nixos/modules>
...
}: {
imports = [
# paths to other modules
];
options = {
# option declarations
};
config = {
# option definitions
};
}
or, with no option declarations, like this:
{ ... }: {
imports = [
# paths to other modules
];
# option definitions
}
or, with no imports or arguments, like this:
{
# option definitions
}
An important property of modules is how they combine into a single final configuration set.
When two different modules define the same option to be two different values, the configuration normally fails to evaluate. But when those values are sets or lists, their values are merged together:
# assuming two declared options `someList` and `someSet`...
# imported.nix
{
someList = [ 1 ];
someSet.a = 1;
}
# main.nix
{
imports = [ ./imported.nix ];
someList = [ 2 ];
someSet.b = 2;
}
# the resulting configuration
{
someList = [ 1 2 ];
someSet = {
a = 1;
b = 2;
};
}
configuration.nix
Okay, so, now that we know how modules work, we can modify our configuration
at /etc/nixos/configuration.nix
, which you'll be delighted to
know is just another module! Specifically, one which does not declare any new
options, just defines existing ones. This kind of module is sometimes referred
to as a profile.
To find out what options exist for you to change, you can
use the search tool
or
take a look around the modules directory. For instance, if you wanted to
use the KDE desktop environment, you might make it so that
services.xserver.desktopManager.plasma5.enable = true
.
Remember that these are normal Nix attribute sets, nothing special, so
a.b.c = true;
is the very same
thing as a = { b = { c = true; }; };
.
Properties
Special functions provided by lib
can also be applied to option
definitions in order to change their behavior during merging.
{ lib, config, ... }:
with lib;
{
## Overriding
# if you want to be able to change a scalar option that's already been defined
# in another module, you can change a definition's override priority to
# signal which should be preferred.
#
# for set/list options, this will also totally replace lower-priority definitions
# rather than merging like normal.
# lower priority = more preferred
# the default is 100
myScalarOption = mkOverride 1 "important!!!";
# presets are also available
myScalarOption = mkDefault "not very important..."; # mkOverride 1000
myScalarOption = mkForce "kind of a big deal!!!"; # mkOverride 50
## Ordering
# if you want to make sure definitions are merged in a *specific order*, you can
# change a definition's order priority, too
# default is 1000
myListOption = mkOrder 1 [ "i go first!!!" ];
# and, the presets
myListOption = mkBefore [ "put me in early :D" ]; # mkOrder 500
myListOption = mkAfter [ "fashionably late ;)" ]; # mkOrder 1500
## Conditional and Assertion
# you can also keep a portion of the configuration from being defined unless
# a predicate is `true`
myMaybeOption = mkIf config.foo.enabled "success!";
# or, more forcefully, you can assert a precondition and throw otherwise
myMaybeOption = mkAssert config.foo.enabled
"you need to enable foo to use this!"
"success!";
## Merge
# finally, you can invoke the merge algorithm on a list of sets yourself, as if
# each of the sets were inside a different module
# this can be convenient in cases where you want to apply different properties to
# different subsets of a config
myMergedConfig = mkMerge [
(mkDefault {
a = 1;
c = 3;
})
(mkIf config.foo.enabled {
b = 2;
c = 4;
})
];
# if config.foo.enabled == true:
# { a = 1; b = 2; c = 4; }
# otherwise:
# { a = 1; c = 3; }
}
Declaring Options
Finally, if you want to make your own option-declaring modules like the ones
NixOS provides, you need to know how to use another library function,
mkOption
:
{ lib, pkgs, ... }:
with lib;
{
options.foo = {
myOption = mkOption {
# all attributes are optional, `null` if unset
# behavior:
default = "world", # what does this option default to if left undefined?
type = types.str, # what type of data is this option?
apply = x: "Hello, ${x}!", # transforms the value given to a different result
readOnly = false, # can this only be written once?
# documentation:
description = ..., # description of option
defaultText = ..., # string representation of `default`
example = ..., # example value
internal = false, # is this for nixos developers only?
visible = true # should this be visible in documentation?
};
# option to enable a module is so common it gets a shortcut
enable = mkEnableOption "foo";
# equivalent to:
enable = mkOption {
default = false;
example = true;
description = "Whether to enable foo.";
type = types.bool;
};
# also, a shortcut for creating a package option
myPackage = mkPackageOption
pkgs # package set
"foo" # package name
{
# all attributes are optional
nullable = false; # whether the package can be null
default = "foo"; # default package, by default the name specified above
example = ...;
extraDescription = ...;
};
};
config = mkIf config.foo.enable {
/* ... */
};
}
Modules outside NixOS
The module system exists in the Nixpkgs standard library, cleanly seperate from the NixOS-specific machinery. As such, now that you know how modules work in general, you can configure your dotfiles in Nix using Home Manager (even on non-NixOS systems), or manage macOS systems with nix-darwin!
You can manage all of your configurations from one single repository using flakes :)