Nix is a deployment system that uses cryptographic
hashes to compute unique paths for components2
that are stored in a read-only directory: the Nix store, at /nix/store/<hash>-<name>
. This
provides several benefits, including concurrent installation of multiple
versions of a package, atomic upgrades, and multiple user environments
[1].
Nix uses a declarative domain-specific language (DSL), also called Nix, to build and configure software. The snippet used to deploy the DNS server is in fact a Nix expression. This example doesn’t demonstrate it, but Nix is Turing complete. Nix does not, however, have a type system.
We use the DSL to write derivations for software,
which describe how to build said software with input components and a
build script. This Nix expression is then ‘instantiated’ to create
‘store derivations’ (.drv
files), which is
the low-level representation of how to build a single component. This
store derivation is ‘realised’ into a built artefact, hereafter referred
to as ‘building’.
Possibly the simplest Nix derivation uses bash
to create a single file containing Hello, World!
:
{ pkgs ? import <nixpkgs> { } }:
builtins.derivation {
name = "hello";
system = builtins.currentSystem;
builder = "${nixpkgs.bash}/bin/bash";
args = [ "-c" ''echo "Hello, World!" > $out'' ];
}
Note that derivation
is a function that we’re calling with one argument, which is a set of
attributes.
We can instantiate this Nix derivation to create a store derivation:
$ nix-instantiate default.nix
/nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv
$ nix show-derivation /nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv
{
"/nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv": {
"outputs": {
"out": {
"path": "/nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello"
}
},
"inputSrcs": [],
"inputDrvs": {
"/nix/store/mnyhjzyk43raa3f44pn77aif738prd2m-bash-5.1-p16.drv": [
"out"
]
},
"system": "x86_64-linux",
"builder": "/nix/store/2r9n7fz1rxq088j6mi5s7izxdria6d5f-bash-5.1-p16/bin/bash",
"args": [ "-c", "echo \"Hello, World!\" > $out" ],
"env": {
"builder": "/nix/store/2r9n7fz1rxq088j6mi5s7izxdria6d5f-bash-5.1-p16/bin/bash",
"name": "hello",
"out": "/nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello",
"system": "x86_64-linux"
}
}
}
And build the store derivation:
$ nix-store --realise /nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv
/nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello
$ cat /nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello
Hello, World!
Most Nix tooling does these two steps together:
$ nix-build default.nix
this derivation will be built:
/nix/store/q5hg3vqby8a9c8pchhjal3la9n7g1m0z-hello.drv
building '/nix/store/q5hg3vqby8a9c8pchhjal3la9n7g1m0z-hello.drv'...
/nix/store/zyrki2hd49am36jwcyjh3xvxvn5j5wml-hello
Nix realisations (hereafter referred to as ‘builds’) are done in isolation to ensure reproducibility. Projects often rely on interacting with package managers to make sure all dependencies are available and may implicitly rely on system configuration at build time. To prevent this, every Nix derivation is built in isolation (without network access or access to the global file system) with only other Nix derivations as inputs.
The name Nix is derived from the Dutch word niks, meaning nothing; build actions do not see anything that has not been explicitly declared as an input [1].
§Nixpkgs
You may have noticed a reference to nixpkgs
in the above derivation. As every input
to a Nix derivation also has to be a Nix derivation, one can imagine the
tedium involved in creating a Nix derivation for every dependency of
your project. However, Nixpkgs3 is a large repository of
software packaged in Nix, where a package is a Nix derivation. We can
use packages from Nixpkgs as inputs to a Nix derivation, as we’ve done
with bash
.
There is also a command line package manager installing packages from Nixpkgs, which is why people often refer to Nix as a package manager. While Nix, and therefore Nix package management, is primarily source-based (since derivations describe how to build software from source), binary deployment is an optimisation of this. Since packages are built in isolation and entirely determined by their inputs, binaries can be transparently deployed by downloading them from a remote server instead of building the derivation locally.

§NixOS
NixOS5 is a Linux distribution
built with Nix from a modular, purely functional specification [2]. It has no traditional filesystem
hierarchy (FSH), like /bin
, /lib
, /usr
, but
instead stores all components in /nix/store
. The system configuration is managed
by Nix and configured with Nix expressions. NixOS modules are Nix files
containing chunks of system configuration that can be composed to build
a full NixOS system6. While many NixOS modules are
provided in the Nixpkgs repository, they can also be written by an
individual user. For example, the expression used to deploy a DNS server
is a NixOS module. Together these modules form the configuration which
builds the Linux system as a Nix derivation.
NixOS minimises global mutable state that – without knowing it – you might rely on being set up in a certain way. For example, you might follow instructions to run a series of shell commands and edit some files to get a piece of software working. You may subsequently be unable to reproduce the result because you’ve forgotten some intricacy or are now using a different version of the software. Nix forces you to encode this in a reproducible way, which is extremely useful for replicating software configurations and deployments, aiming to solve the ‘It works on my machine’ problem. Docker is often used to fix this configuration problem, but Nix aims to be more reproducible. This can be frustrating at times because it can make it harder to get a project off the ground, but I’ve found the benefits outweigh the downsides, personally.
My own NixOS configuration is publicly
available7. This makes it simple to reproduce
my system (a collection of various hacks, scripts, and workarounds) on
another machine. I use it to manage servers, workstations, and more.
Compared to my previous approach of maintaining a Git repository of
dotfiles
, this is much more modular,
reproducible, and flexible. And if you want to deploy some new piece of
software or service, it can be as easy as changing a single line in your
system configuration.
Despite these advantages, the reason I
switched to NixOS from Arch Linux was simpler: NixOS allows rollbacks
and atomic upgrades. As Arch packages bleeding-edge software with
rolling updates, it would frequently happen that some new version of
something I was using would break. Arch has one global coherent package
set, so to avoid complications with solving dependency versions Arch
doesn’t support partial upgrades. Given this, the options were to wait
for the bug to be fixed or manually rollback all the updated packages by
inspecting the pacman
log (the Arch
package manager) and reinstalling the old versions from the local cache.
While there may be tools on top of pacman
to improve this, the straw that broke the camel’s back was when my
machine crashed while updating the Linux kernel, and I had to reinstall
it from a live USB.
While Nixpkgs also has one global coherent package set, one can use multiple instances of Nixpkgs (i.e., channels) at once to support partial upgrades, as the Nix store allows multiple versions of a dependency to be stored. This also supports atomic upgrades, as all the software’s old versions can be kept until garbage collection. The pointers to the new packages are only updated when the installation succeeds, so the crash during the Linux kernel upgrade would not have broken my OS install on NixOS. And every new system configuration creates a GRUB entry, so you can boot previous systems even from your UEFI/BIOS.
To summarise the parts of the Nix ecosystem that we’ve discussed:
§Flakes
Without going into too much depth, Flakes
enable hermetic evaluation of Nix expressions and provide a standard way
to compose Nix projects. With flakes, instead of using a Nixpkgs
repository version from a ‘channel’8, we pin Nixpkgs as an
input to every Nix flake, be it a project build with Nix or a NixOS
system. Integrated with flakes, there is also a new nix
command aimed at improving the UI of Nix.
You can read more detail about flakes in a series of blog posts by Eelco
on the topic9.
§References
As ‘nix’ means snow in Latin. Credits to Tim Cuthbertson.↩︎
NB: we will use component, dependency, and package somewhat interchangeably in this blog post, as they all fundamentally mean the same thing – a piece of software.↩︎
nixos.org/manual/nix/stable/package-management/channels.html↩︎