Nix

Published Wed 14 Dec 2022. Last update Mon 3 Feb 2025.
A post in the series.
Nix snowflake1

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.

Visualisation of Nixpkgs4

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

[1]
E. Dolstra, M. de Jonge, and E. Visser, “Nix: A Safe and Policy-Free System for Software Deployment,” p. 14, 2004.
[2]
E. Dolstra and A. Löh, NixOS: A purely functional Linux distribution,” SIGPLAN Not., vol. 43, no. 9, pp. 367–378, Sep. 2008, doi: 10.1145/1411203.1411255. [Online]. Available: https://doi.org/10.1145/1411203.1411255. [Accessed: Oct. 13, 2022]

  1. As ‘nix’ means snow in Latin. Credits to Tim Cuthbertson.↩︎

  2. 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.↩︎

  3. github.com/nixos/nixpkgs↩︎

  4. www.tweag.ioblog/2022-09-13-nixpkgs-graph↩︎

  5. nixos.org↩︎

  6. NixOS manual Chapter 66. Writing NixOS Modules.↩︎

  7. github.com/RyanGibb/nixos↩︎

  8. nixos.org/manual/nix/stable/package-management/channels.html↩︎

  9. tweag.io/blog/2020-05-25-flakes↩︎