Introduction to the Nix ecosystem

A simple overview of a new DevOps superpower.

Birds eye view

Here we are today with a new tool that’s making waves in the DevOps world - Nix. We spent some time using it and have some impressions that might help you get started with the whole ecosystem as well as make the decisions if it’s worth diving at all.

The first question that we would usually ask ourselves is - what is this thing called Nix? Here things get a bit more tricky and that’s precisely why we thought that we need to create this article. Nix is many things. Surprisingly, most of the thing it does, it does pretty well. The problem is, that when you are thrown into its world for the first time it’s a bit hard to distinguish between all the concepts that get squeezed together. If you bounce back from this initial wall, you might lose the opportunity to use one of the most interesting tools of the last few years.

Ok, so finally - WHAT IS NIX? Nix is a bunch of things that share the same name and work in unison to make it possible to create reproducible development environments. There. As the official webpage of NixOS states: Nix is a tool that takes a unique approach to package management and system configuration. Although this does not explain very much, we assure you that the documentation is very good, and recommend checking it out after reading this article.

So what things are part of the Nix world? The main ones are:

  • nix commands (a set of shell utilities)
  • nix language (pure, lazy-evaluated, functional, used to describe packages)
  • nix packages (nixpkgs, a repository of nix expressions and binary packages)
  • nix OS (a Linux distro that’s described and built using just Nix tooling!)

There are several smaller tools and projects that are associated with Nix, but let’s keep things as simple as possible for now. With what we have to disposition so far, we already have a lot of power.

What are the common use cases for Nix? We can for example:

  • create a temporary shell containing all packages and dependencies necessary for a specific task
  • make sure that all the dependencies required by developers are present and in correct versions
  • provide the same package in different versions for multiple users on one system
  • upgrade a package and then rollback to the previous version
  • describe and publish a deterministic recipe for producing a package
  • create a Docker image based on the package description
  • tailor your own minimalistic Linux distro
  • build binary packages with all dependencies that can be copied over manually to different systems

If you already know Anaconda and conda-pack then some of those things might seem familiar. Nix can manage packaging things on a system level and across different languages. If you could install something from the command line, then you can most likely do it declaratively with Nix.

It all sounds nice and cozy, but is it worth learning this new tech? Here are some pros and cons for you to decide.

Pros:

  • very powerful thanks to a variety of things bundled together
  • it’s quite easy to do common things
  • fast and reliable
  • over 60k packages available in nixpkgs
  • active and growing community
  • one tool to rule them all

Cons:

  • easy to get lost at the beginning (we are here to help!)
  • you have to learn nix-lang
  • yet another tool
  • you still have to understand all the tech described by a package to use it (well, there is no going around this one)
  • does not support Windows

Overall we think that Nix is worth trying, but if you have limited time or do not enjoy tinkering with new tech as much, then you might want to wait a year or two to see if the project is still alive. It does seem like a well-balanced compromise between a Cathedral and a Bazaar, the only thing that feels like a chore is learning nix-lang, which you will have to do if you want to do some more complex stuff.

Installing

To install nix, please check out this link. The recommended way to set up (multi-user) should work for just about anyone. From our side, we can say that Nix has one of the most polite installers out there. You will have the information about every operation that needs to be performed. That being said, you might want to do a restart after installation to make sure everything works as expected. Full reboot seems to be required, at least it was for our setup, but it’s not mentioned in the documentation.

Nix commands

Let’s get down to a more fine-grained perspective now and see what Nix commands are all about. There are three main ones:

  • nix-env - allows to change environment (e.g. install packages)
  • nix-build - builds nix expressions/derivations (just fancy names for dependency descriptions)
  • nix-shell - starts a shell based on a nix expression

Full documentation can be found here.

All the following examples were tested on a Linux dist. Although it is possible to use Nix on Mac, there is no magic when just creating an enhanced shell - all the dependencies must be available for the given system. We can go around it by using NixOS from Docker, but that’s a bit too much for starters.

Nix shell

Ok, please create an empty directory, cd inside, and run nix-shell -p hello. The -p option lets us specify a package from nixpkgs that we want to download and run. In the shell that opened you can run hello command. Provided that you did not have hello installed on your local system beforehand, we have proof that the shell provided the necessary binary for you. If you type exit and try running hello again, it won’t work - Nix did not make any changes outside of this temporary environment.

You might have noticed that when running nix-shell you got some additional output in a similar form: these paths will be fetched (0.04 MiB download, 0.20 MiB unpacked). What happened is that Nix had to first download the packages that you requested. If you would run the same command the second time, you would not see the same message. All the necessary binaries are already stored locally on your machine. Nix shells are fun when you are trying some new tool and you can enhance them even further by using direnv (look here). This handy little tool can switch between profiles automatically when navigating directories, meaning that you can easily achieve separate environment activation for separate projects. Never again mess up your setup by forgetting to switch profiles!

Nix env

Ok, that was working with temporary shells, the innermost layer of the onion, but we can go one layer up and work on system-level “profiles” as well. Confusingly enough those are called environments and it’s possible to manipulate them using nix-env command.

Try running nix-env -q - if you did not change anything by yourself, it should return nothing at all. This is the current state of packages that we have installed in our “global” user profile. Please run nix-env -i hello now. What you will get should resemble something like this:

installing 'hello-2.10'
building '/nix/store/kmq8d1h3ps10fazi2n5cncn2h8q8j3f4-user-environment.drv'...
created 32 symlinks in user environment

You can run hello from any shell on your system now. If you re-run nix-env -q, you will see hello proudly listed as one of your environment dependencies. When you are done reading what hello has to say, you might want to remove it by typing in nix-env -e hello.

We learned how to install and uninstall packages, but what was all this output when installing hello? Why did it say that it builds something and what was this weird path? Well, that’s probably the most interesting thing about how Nix works.

Nix store

When we say Nix store, we refer to what Nix is keeping locally on our machine and what provides us with necessary binaries. You might have thought that when we run install or uninstall with nix-env then we directly manipulate the store, but it’s only partially true. What happens is that in the case of this command (or nix-shell) we are only accessing a tailored view of our local repository. Before we understand what that means, we have to first look at packages in general.

We have heard a few concepts thrown around. Let’s maybe start with nix expression. This particular thing is just a function in nix-lang that sits somewhere in some file and is used to describe a package. The term package is a tricky one because it’s not 100% the same thing as an RPM or pip “package”.

All the packages that we can install sit in the repository called nixpkgs. You can search for the “hello” package using nix-env -qaP hello, you can go to online search or directly to the repository. When we are saying “repository”, we not only mean it as in “git repository”, but as in “nix repository” as well. Nix packages in some sense are just that - files describing how to build a binary in a deterministic way.

Each .nix file specifies, among other things, a URL for fetching sources and SHA checksum for this tarball. When building a binary package with Nix, we calculate a checksum of the whole .nix file and append it to the package name, so changing any dependency or build step will result in producing a different package. As we write definitions for new packages, we can import any other package that we need, which makes the whole nixpkgs just a big recurrent structure.

This is all very reliable in some sense - it allows us to produce deterministic (as far as the network allows) artifacts and do not worry about any dependency changing downstream. There are unfortunately two problems visible at first sight.

First of all, we would go crazy trying to build everything from scratch each time. Remember - dependencies in nixpkgs point to things that also are Nix packages, therefore those must be built before we can proceed. To solve this issue nixpkgs is automatically using a pre-built cache.

The second problem is similar - if we have multiple users, or try to spawn many different Nix shells, then we would have to download the same things over and over again. This is solved by keeping one global repository that contains all the artifacts in different versions and treating each environment or shell as a view of this structure. This is precisely what we see when we use nix-env -q. What happened when we installed the hello package is that we downloaded it to Nix store and pointed from our environment to its location. When we uninstalled the package, we did not delete it completely, just forgot the symlink in this one specific environment. If we would try to install hello from any different environment, we would not have to download anything again. Obviously, this can lead to a lot of bloat on the hard drive, but we can remove all unused things with nix-store command.

Let’s quickly dive a bit deeper. After executing ls -l /nix/store you will see all the packages that are currently installed on your system (among other things). The structure is completely flat - there are no groups or organizations. What we have instead is the before-mentioned checksum of Nix expression prepended to package name. If you look closely (or use grep) then you may notice that some entries resemble part of the output that we got when installing hello:

building '/nix/store/kmq8d1h3ps10fazi2n5cncn2h8q8j3f4-user-environment.drv'...

All of the directories that end with -user-environment contain “views” that aggregate all the remaining packages. If you execute ls -l /nix/var/nix/profiles/per-user/[your username here]/ then you will get a structure similar to that:

lrwxrwxrwx 1 xxx xxx 60 Sep 23 08:10 profile -> profile-2-link
lrwxrwxrwx 1 xxx xxx 15 Sep 23 08:00 profile-1-link -> /nix/store/5yvn2938arc9pkg2dalrmp76ybxp1xzw-user-environment
lrwxrwxrwx 1 xxx xxx 60 Sep 23 08:10 profile-2-link -> /nix/store/clyqlqfsqbsrqmr7yxkqjaq8hn6qi72z-user-environment

This is how Nix “composes” your current environment. The profile symlink points to the current package subset that is in use and is visible from the default shell. Since all entries in /nix/store are effectively immutable (we do not delete packages when uninstalling!) then we can keep the symlinks for as long as we need and can perform…

Nix upgrades and rollbacks

The default command for upgrading packages in Nix is nix-env -u [package name]. Unfortunately, it’s a bit hard to see it at work, since we would have to install hello and wait for the next version to come out, which would make this post a bit too long. What we can do instead, is to take a look at rollbacks and “generations”. We’ve seen that the user profile is just a symlink that points to another symlink, which in turn points to some set of packages. By changing where the profile points, we can effectively change what packages are available (and in what versions, if we did an upgrade).

Fortunately, we do not have to track the symlinks by ourselves, Nix comes packaged (excuse the pun) with one more abstraction that we can see when running nix-env --list-generations. What we get is something like that:

   1   2021-09-23 08:00:00   
   2   2021-09-23 08:10:00   (current)

This is nothing else, but a more user-friendly representation of our /nix/var/nix/profiles/per-user/[username]/ directory. In this view, the “current” entry corresponds to our “profile” symlink. The simplest way to switch to a previous system state (a “generation”) is to use nix-env --rollback. This will move the cursor one position back (from 2 to 1 in this case). We can also choose a specific generation by calling nix-env --switch-generation [index of generation]. Currently we have the hello package installed. Let’s remove it by running nix-env -e hello. If we would check our generations again, we would get something like this:

   1   2021-09-23 08:00:00   
   2   2021-09-23 08:10:00
   3   2021-09-23 08:20:00   (current)

Let’s go back to generation 2, either by nix-env --rollback or nix-env --switch-generation 2. Now we have:

   1   2021-09-23 08:00:00   
   2   2021-09-23 08:10:00   (current)
   3   2021-09-23 08:20:00   

Ok, so what happens if we try to run nix-env -e hello again? As a good clickbait says - the answer might surprise you! If everything goes according to plan, you will get back:

   1   2021-09-23 08:00:00   
   2   2021-09-23 08:10:00   
   3   2021-09-23 08:20:00   (current)

We did not install anything new, we just moved our cursor! How does Nix know that we can reuse an old environment setup? Well, that’s a pretty simple question - because symlinks are pointing to directories in /nix/store and those already have checksums calculated. When running Nix commands we always have all the requirements for our environment defined. If we need to create a package or an environment, then we will do it only once and reuse the result in any further attempts. That’s the power of determinism.

Ok, now that we have most of the basic elements of Nix covered up, then let’s start…

Building custom Nix packages

As every good entrepreneur knows it’s usually better to improve an existing thing that has its established market than to try creating a new niche out of nothing. Since we are all obedient capitalistic cogs here, we will try to do just that. After consulting our marketing department it turned out that the hello command that we previously used can reach a much more wide audience if we change the name of the package to wow and run an ad campaign.

Since we expect that there might be an iterative process of development and can’t predict the impact on the system, we would like to play it extra safe and choose to facilitate Nix powers. How would we embark on this journey?

First, let’s create a new folder and cd into it. The name is not important, you might call it for example src. Then, create a new file called default.nix and paste in this content:

with import <nixpkgs> {};

stdenv.mkDerivation {

  pname = "wow";
  version = "0.1";

  src = fetchurl {
    url = "mirror://gnu/hello/hello-2.10.tar.gz";
    sha256 = "0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i";
  };

}

Now, run nix-build. You will get a lot of output and at the end something of the form:

/nix/store/1zch4lvyhxn829mai2vfldp16h6a72i6-wow-0.1

The package is now present in the store. Additionally, you will get a result symlink pointing to it from the directory in which you run the build. If you run nix-env -q then this package will not appear on the list - we did not install it in the default env. Let’s try adding it by using nix-env -i wow.

Unfortunately, you will get an error. This operation does not work, because by default we are trying to access remote nixpkgs to query for a specific package name. The simplest way to install in our case is to either point to the result - nix-env -i ./result or directly from expression - nix-env -f ./default.nix -i. In both cases, the package itself will be recognized as already built and taken directly from the store. Depending on the state of your environment, you may have a conflict with the hello package, in which case just roll back to the previous generation.

Now, we can even instantiate a shell that will contain our custom package. Please run nix-shell -p ./result git. This will create a temporary shell containing both of those commands. You can test if wow is present by running hello.

Advanced Nix features

Ok, we’ve seen some pretty useful things that one can do with Nix, but we did not solve one very important problem - sometimes it’s not enough to guarantee that all the dependencies will be installed on a given system, we need to make sure that the whole system is in a specific state. An example of such a use case might be provisioning a CI/CD agent, or deploying a package containing a microservice.

What can be done about this? There are two approaches that we can take.

The first one is to create a docker image based on a specific package. It is possible to reference Nix packages that we have imported from inside the image and run them as a command. This is a very useful feature and it will most likely cover our microservices scenario, but we can go even deeper than that and reach for NixOS.

NixOS is a Linux distribution that is fully defined using Nix packages. The system itself supports a generation system as in the case of Nix store. Each of the generations is treated as a fully functional system and can be booted in GRUB. When making changes to the packages of our system, we are effectively appending new generations. This way any upgrade and rollback is extremely safe and easy. All of the options can be controlled from /etc/nixos/configuration.nix file, which contains a regular Nix expression describing our system dependencies.

As a cherry on top, you can even deploy NixOS using Terraform. Remember our previous posts? If not, then maybe it’s time to revisit our introduction to Terraform.

Summary

I hope that we got your attention and interest concerning Nix. It’s a really interesting piece of technology and a nice thing to add to a DevOps toolkit (or any toolkit for that matter). Although there is a bit too much information concerning the Nix world and not everything is obvious at first sight, the core system is well thought out and seems to work as expected. As the project matures further, we are sure that all of the confusion will gradually be worn down. With a community as big as it is now, we probably don’t have to worry that we will never hear about this tool again.

Thank you for your time and we hope that you will come back to read more of our posts!