Exploring Nix

David Wagner

For the last few weeks I’ve been exploring NixOS and its related tools. This article is an experience report and a collection of learning material I find useful.

NixOS is a unique Linux distribution with origins in research conducted at Utrecht University. NixOS handles software delivery and configuration in a different way than any other distribution I know. The entire operating system is treated as an immutable value which makes deploying and maintaining NixOS-based systems easy and reliable.

The language

NixOS is configured using the Nix expression language: a small, purely functional programming language. Besides the usual data types (booleans, numbers, strings, lists and sets) Nix has some features which are uncommon in configuration languages.

Let blocks bind values to symbols. The bindings appear after the keyword let and the symbols can be used after the keyword in. For example, the expression:

let
  x = "a";
  y = "b";
in
  x + y + x

evaluates to "aba".

In Nix, functions are first-class values. A function which adds one to a value is defined succinctly:

n: n + 1

Typically, functions take sets as arguments and return sets as results. This expression defines and calls the endpoints function:

let
  endpoints = { engine, domain }: {
    http = "http://${engine}.${domain}";
    https = "https://${engine}.${domain}";
  };
in
  endpoints { engine = "google"; domain = "com"; }

which evaluates to:

{ http = "http://google.com"; https = "https://google.com"; }

This last example also shows how symbols in the current scope are interpolated in strings using ${}.

These constructs are sufficient to read the examples in this article. For a comprehensive overview of the language features see this tutorial.

Derivations

A derivation, a core concept of Nix, is a build recipe: it describes how to obtain, in other words derive, a component from its inputs. Let’s make this statement more concrete with an example.

We will use Nix to put a string into a file using a build action equivalent to:

echo hello from Nix > output.txt

This build runs the echo shell command to generate a file. The build requires no input source files, but a shell to be present. We could use bash but from where do we get its executable?

Typical build systems assume that certain programs are available in the build environment. Nix doesn’t make such assumptions. Builds are performed in isolation: no programs, no environment variables, nor access to the outside world are available unless explicitly specified.

In programming we use a function to abstract over an input parameter of a computation. Let’s do the same and write a function which takes bash as input and returns the build recipe, a derivation:

# hello.nix
{ bash } :

derivation {
  name = "hello";
  builder = "${bash}/bin/bash";
  args = [ "-c" "echo hello from Nix > $out" ];
  system = builtins.currentSystem;
}

derivation is keyword of the Nix language. It’s represented as a set with specific attributes: a name, a program and its arguments to produce some output, and a specification of the operating system’s architecture where this derivation can be built.

The bash argument is a dependency, it refers to a derivation which must be built before the hello derivation.

To understand better the derivation’s structure, let’s assume we have bash built and we evaluate the hello derivation. Nix stores the resulting derivation in the following structure:

{
  "/nix/store/61lcv6k65f42d3v8nww7m7k48h7v9mhy-hello.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/qcnf97fclrnqppq3h5ld9smqdb8l2ybk-hello"
      }
    },
    "inputSrcs": [],
    "inputDrvs": {
      "/nix/store/8ynv2wxv6vaa75sbpmz8rnlbv1bxcfzn-bash-4.4-p23.drv": [
        "out"
      ]
    },
    "platform": "x86_64-linux",
    "builder": "/nix/store/9si14qjcz8072c0v42zbkglq08s2cg04-bash-4.4-p23/bin/bash",
    "args": [
      "-c",
      "echo hello from Nix > $out"
    ],
    "env": {
      "builder": "/nix/store/9si14qjcz8072c0v42zbkglq08s2cg04-bash-4.4-p23/bin/bash",
      "name": "hello",
      "out": "/nix/store/qcnf97fclrnqppq3h5ld9smqdb8l2ybk-hello",
      "system": "x86_64-linux"
    }
  }
}

In this representation we can see how Nix stores the build artifacts under /nix/store. The filenames in the store are cryptographic hashes of the defining Nix expression suffixed with a readable, user-provided name.

This data structure is a concrete description how to build the hello greeting:

  • Outputs: the derivation yields one output named out. The output will be stored under the given path.

  • InputSrcs: the derivation requires no input sources

  • InputDrvs: the derivation depends on the derivation which builds bash.

  • Platform: on my PC the expression builtin.currentSystem evaluates to x86_64_linux.

  • Builder and args: the program and its arguments we provided, but now referencing concrete artifacts in the store.

  • Env: the environment variables available during build. These are originating from the derivation’s attributes we provided in the Nix expression. We reference $out in the build script.

Derivations are just data without any knowledge of the Nix language. When evaluated, a Nix expression typically produces many, interdependent derivations which are built in topological order, that is each derivation is built after their dependent derivations.

Nix builds a component in two stages:

  1. Evaluates the expression and writes resulting derivations in the Nix store.
  2. Builds the derivation and writes build results in the Nix store.

Nix expressions provide a high-level language for developers to define components, component configuration and component relationships. Derivations encode build instructions for a single component for a given configuration in a well-defined environment.

Nix expressions can be evaluated on any system where Nix is installed. Derivations may be copied to other nodes for building: to a build farm, or to nodes with special hardware.

Build the example

To build the example in the previous section, save the expression in a file named hello.nix and run:

$ nix-build hello.nix --arg bash '(import <nixpkgs> {}).bash'
/nix/store/qcnf97fclrnqppq3h5ld9smqdb8l2ybk-hello

The --arg switch tells Nix to use the bash package from the Nix Packages collection. nix-build prints the path where the build result is stored. By default, nix-build also creates a symbolic link to the build result in the current working directory so it’s easy to verify if we find the string we expect:

$ < result
hello from Nix

To see the internal structure of the derivation, run the command:

nix show-derivation /nix/store/qcnf97fclrnqppq3h5ld9smqdb8l2ybk-hello

which outputs data structure as shown in the previous section.

Composing an operating system

The Nix language provides a clean component description formalism: a single expression builds 40000 packages on multiple platforms. To achieve this scale, higher-level abstractions are built from the Nix language primitives.

Modules are functions which return a set with specific attributes:

{ config, pkgs, ... }:

{
  imports = [
    # paths to other modules
  ];

  options = {
    # option declarations
  };

  config = {
    # option definitions
  };
}

Modules are useful for configuring complete subsystems such as networking, printing, graphics and so on. The top-level NixOS configuration, typically stored in /etc/nixos/configuration.nix, is a module itself which may include other modules.

For example, the firewall module allows you to specify the open ports of your system:

networking.firewall.allowedTCPPorts = [ 80 ];

The module keeps the intricacies of generating iptables rules within its implementation. Our system configuration remains simple and declarative.

Applications

The existence of NixOS proves that the Nix expression language is a solid foundation for software packaging and software delivery. NixOS is not the most popular Linux distribution today, but if you felt the pain of server management using tools like Puppet, Salt or Ansible, you should definitely give NixOS a try.

You don’t have to replace your operating system to try Nix. You can use Nix packages on Linux and Mac systems to set up and share build environments for your projects, regardless of what programming languages and tools you’re using.

The Nix model can also be used to deploy servers and for continuous integration and delivery.

Learn more

This section is a collection of online resources which I find useful to learn about Nix.

Learn the Nix language:

Learn about modules and overlays:

Browse Nix packages and NixOS options:

Read about the original research:

Additionally, the NixOS Wiki contains a list of learning resources.

Summary

After a few weeks of learning I’m amazed by Nix. I believe NixOS is the best operating system to deploy from a declarative configuration. The expression language, the Nix store, the evaluation and build strategy were built for painless software deployments.

If you value infrastructure as code and immutable deployments you should definitely spend time on Nix and its core concepts.

I updated my main laptop and my servers at home to NixOS. The configuration of all my machines is available on GitHub.