About two months ago, when switching to a different computer, I have decided to finally install NixOS and give it a try. I have been doing all my usual work on it since then, and I think I can start sharing what I have learned so far.

This was a natural move for me, since I was already using the nix package manager on my machine, then running Fedora, for about two years — I installed all audio software via nix1, from SuperCollider to jack to Ardour. Nixpkgs, the main package collection of nix, offered a wider and more recent selection of audio software than Fedora (with Planet CCRMA).

NixOS is a complete linux distribution based on nix, currently offering about 60000 packages.

Disclaimer: I’m still a NixOS newbie, so my understanding of it may be naive-ish; yet that is in part why I have decided to start writing now - before I forget how it was using the system fresh, adapting to the new environment and studying it deeper as I write.

Much of what I describe below also applies to Gnu Guix, a distribution built on the same architecture as nix and NixOS, but using a lisp language as its main fabric instead of nix. (In fact, I anticipate making a switch to Guix some day.)

Both NixOS and Guix are oriented towards using the command line and a text editor to manage your system; and you probably need to have a basic understanding of the main building blocks of a unix-like system to feel comfortable.

Screen shot of the NixOS installation live image with three icons on the desktop: GParted, Konsole, NixOS Manual.
The NixOS live image (on the screenshot) doesn't provide a graphical installer, but it provides the needed tools to partition the disk and create filesystems according your needs. Then the system is installed via a command line program.

So what is NixOS, after all?

The core ideas

Append, not overwrite

Ever since I have started using git, I fell in love with the idea of immutability: the beauty of creating new versions of things instead of modifying things in-place. After you did a git commit, you know any edits you make will not ruin what you already have. I now recognize it in many places - functional programming languages, copy-on-write filesystems, backup and file synchronization tools, package managers, or Wikipedia. And I tend to gravitate towards tools and systems that leverage this idea.

When you install, upgrade or remove a package with nix, it is somewhat similar to making a commit in git - it creates a new version of the environment with the required packages added or removed. Then it just switches to the new environment by updating a symlink.

The previous version of the environment stays there should you want to rollback. Each version of the environment is called a generation. Unlike git, you can ask nix to remove some or all of the previous generations to save disk space (sure you wouldn’t want to keep past versions of software installed forever.) But it won’t delete anything by itself, only when you explicitly ask it to.

An environment is basically just a directory tree resembling the traditional /usr but with symlinks instead of actual binaries; the binaries themselves are stored in a special directory called the nix store:

$ ls ~/.nix-profile/
bin  etc  include  lib  lib64  libexec  manifest.nix  sbin  share
$ which kdenlive
/home/me/.nix-profile/bin/kdenlive # < this is a symlink to /nix/store:
$ realpath $(which kdenlive)
/nix/store/hn2pv0hci1d9c9b5rd0b9g0css0ydg74-kdenlive-20.04.3/bin/kdenlive

A succession of environment generations is called a profile. A profile is, in a way, like a git branch - it is a pointer to a specific version of an environment:

$ readlink ~/.nix-profile  # my current user profile
/nix/var/nix/profiles/per-user/me/profile
$ readlink /nix/var/nix/profiles/per-user/me/profile  # ...points to generation 95
profile-95-link
$ readlink /nix/var/nix/profiles/per-user/me/profile-95-link  # ...of the environment
/nix/store/8y9k19xhs6v4v97k8wpqc0xjklfqrvq8-user-environment

There are two kinds of profiles on NixOS - system and per-user. System profile provides common packages for all users; each user may additionally install packages into their own profile without affecting other users.

$ which kdenlive  # installed my user profile:
/home/me/.nix-profile/bin/kdenlive
$ which bash  # installed system-wide:
/run/current-system/sw/bin/bash

Also, user A might decide to install a different version a package than user B, or even a different build of the exact same version of the package. Both packages will peacefully coexist, because:

Different build, different hash, different path

The actual files of each package are stored in a separate directory per package:

/nix/store/<long-hash>-package-version/

…and symlinked into your environment when you install a package.

Each build of a package produces a hash based on all the parameters of the build (including the hash of the downloaded source code archive) as well as all of its dependencies. So if I ask nix to rebuild ffmpeg with a different configure flag, or with a different version of any dependency, or if it is a different version of ffmpeg itself, it will get a different hash, and will appear at a different path in /nix/store/.

Also, that particular build of ffmpeg will in turn depend on the particular build of glibc in /nix/store/ it was compiled against. That means one can have different versions of ffmpeg (and glibc) available on a system simultaneously and switch between them on demand.

Which version of a package you have installed depends on which channel you installed it from. For instance, there is a stable channel that corresponds to the latest NixOS release (nixos-20.03 at the time of writing), and an unstable channel providing the most recent versions of software.

Most of the packages on my system come from the stable channel, but occassionally I would install a package from the unstable channel in my user profile. This allows me to easily access the most bleeding-edge packages should I need to, while also being able to revert to a stable version at any time.2

$ nix-env --query --available --attr-path kdenlive
nixos.kdeApplications.kdenlive     kdenlive-19.12.3
unstable.kdeApplications.kdenlive  kdenlive-20.04.3
$ kdenlive --version  # the system-wide kdenlive
kdenlive 19.12.3
$ nix-env --install kdenlive-20.04.3  # builds a new user environment with kdenlive 20
installing 'kdenlive-20.04.3'
building '/nix/store/7xd3gw6yqk7c41na9b8w87ql71l732s9-user-environment.drv'...
created 1533 symlinks in user environment
$ kdenlive --version  # affects my user only
kdenlive 20.04.3
$ nix-env --rollback
switching from generation 97 to 96
$ kdenlive --version  # back to system-wide kdenlive
kdenlive 19.12.3

Get to the source

Nix is a source-based package manager, meaning that a package is not a bunch of binaries compressed in an archive, but a precise description of how to derive the binary (or bytecode, or whatever) from source code.

When you install a package, nix asks a server whether a prebuilt binary substitute of a package is available. If there is one, it will be used instead of building from source on your machine. Otherwise, nix will compile the package from start to finish.

Nix strives to be fully reproducible (meaning that the resulting binaries should be bit-to-bit identical between machines), but not all packages are there yet.

Nevertheless, this ability to easily rebuild a package from source is very handy when you want to customize a package, for instance by changing the configure flags or enabling an optional build dependency. A build happens in an isolated environment, so that none of the development dependencies will pollute your working environment.

In effect, the user experience of installing a package from source is the same as when installing a prebuilt binary package. You just ask nix to install your customized definition of a package and it automatically builds and installs it for you.

A generated, versioned system

I have been managing my Fedora install (as well as remote systems) with Ansible for some time - I like the idea of having a concise description of the configuration of a system in one place (as opposed to having to remember it), and I hate doing things twice. Ansible automates boring repetitive tasks (like disabling password logins on a server) well for me.

I don’t need to use Ansible to manage NixOS though, since the system itself is based on declarative configuration. This is how you install NixOS - you edit a configuration file describing the to-be system (the file is conveniently pregenerated by the installer to reflect the hardware configuration of your machine) and then the installer uses nix to build the system accordingly.

Later, when you want to change something (add or remove a user, a package, a service, flip a setting), you edit the file again and run sudo nixos rebuild switch. This builds a new generation of the system profile (just like with user package profiles) and switches the system to it on the fly.

The system profile includes much more than just the list of installed packages. It also defines the contents of files like /etc/passwd, /etc/sudoers, /etc/profile or even /etc/fstab; it contains the configuration of systemd services, firewall, runtime kernel parameters, bootloader configuration, locales, etc.

While Ansible is about modifying a system, NixOS generates one instead. What this means is that if you remove something from the configuration (like a package or service), it disappears from (current generation of) the system too, or is reset to its default clean state.

If anything goes wrong after you have made a change, you can do sudo nixos rebuild switch --rollback to go back to the previous generation; additonally, the GRUB boot menu provides a list of past configurations that you can boot into just in case.

While this might not be for everyone, I find this a very natural upgrade from using Ansible on my own system. And if I need to manually put something into /etc, I just add the relevant section to my configuration.nix to do that for me.

Caveats

NixOS prioritizes certain properties of the system over others; controlling and isolating software compilation, minimizing undeclared modification to a system is certainly an important aspect of its design.

Depending on your use case, this might or might not be what you want.

For instance, this will most likely make curl http://example.com/some-random-script.sh | sudo sh fail, as the system environment is quite different from what such a script would probably expect (for instance, /usr is not in $PATH and is mostly empty).

This also immobilizes most binary software not specifically compiled for NixOS, as that would expect shared libraries in common locations, whereas on NixOS they are stored in /nix/store/ to make multiple versions of shared libraries coexist.

This is preferable for me, as I avoid running third-party binaries (or piping curl into sudo) for security reasons anyway. (I only do things like sudo npm -g install some-popular-package in temporary isolated containers if I need to.) Nor do I run closed-source software, although there are various ways of making precompiled binaries run on NixOS, if you must.

Compiling software from source also needs to be done in an environment prepared by nix. Again, that may or may not be for you. I find this to be a clean way to manage build dependencies, but it needs a bit of getting used to.

Overall, NixOS is for use cases where you want to have a managed system with a declarative configuration and the possibility to easily alternate between different versions2 and/or builds of packages (including custom builds).

While in this post I allowed myself to speak in a fairly general way, I hope this it was helpful as a birds-eye view. I’m looking forward to writing more about specific, practical aspects of nix and NixOS in my next posts.

A screen shot of Ardour5 main window with midi tracks and automation curves, a Calf Vintage Delay effect, a qJackCtl's main and Graph windows.
Making some sounds on NixOS.
  1. By the way, software installed via nix doesn’t touch /usr at all, so it won’t interefere with your distribution. 

  2. Note that while nix provides the means to have as many different versions of packages (including their differing dependencies) on a system as one likes, the main package collection (nixpkgs) is not aimed at providing arbitrary versions of all packages to install; most packages have a single version available in a given channel at a given moment in time. There are ways of installing older versions of packages that don’t match what’s currently in a channel - and thanks to nix, that won’t mess up your system (you can always --rollback or use nix-shell).  2