Like most modern software companies, we use continuous integration (CI) to build, test and deploy our software after we make changes to our code base. Naturally, this requires a build server that has all the build and test dependencies instOalled. The problem: there are vastly different requirements for building and testing software. C/C++ needs a compiler like gcc, Java needs at least a JDK, but more often than not a build tool like Maven. LaTeX needs something like Texlive, and so on.

Moreover, what you should strive for are reproducible builds. No matter what date, server setup or, say, internet availability, your build should always produce the same results.

Possible solutions

Use your distribution

If your CI system provides this, you can trigger the CI build process directly on your build server as a normal user (for example, by registering a shell runner in Gitlab CI). This way, you install all the build dependencies using the package manager (e.g. apt-get, dnf, pacman) of your distribution. This solution has a few pros and cons.

Pro: You know what you’re doing. The distribution on your server is something you’re familiar with. Don’t underestimate this.

Pro: Building and installing packages for your server distribution is simple, as the tools are readily available (for instance, makepkg on Arch).

Pro: Deploying software might be simple, as you can scp to other servers or even copy results on the machine you’re running on without further adjustment.

Con: Unless you’re giving the CI user root privileges, you’re not able to install the necessary dependencies inside your source code repository. You have to take care of installing the right software before the CI server starts building it (and you have to uninstall software that’s no longer needed by any project).

Con: Keeping the build reproducible is harder to achieve, since you’re a good administrator and keep on updating the software on your servers. Different versions of your build tools might change the outcome of your build.

Con: In the same vein, reproducing the CI build locally requires that you install the exact same software packages on your developer machine. If you have a different distribution on your local machine than your server has, this is pretty difficult. Also, it might be difficult to install multiple versions of the same package on the same machine. Think about a project that needs an older version of the build system, and a different project that needs the latest one. Not all distributions/packages support parallel installs.

Use Docker

Some CI systems like Gitlab CI or Travis support specifying a Docker image that’s used for building and testing your software. Again, here are the pros/cons of doing that.

Pro: The build and test dependencies are all installed in the Docker image and thus have a version that’s independent of the server environment you’re running Docker from. Also, Docker provides network and file-system isolation. This is helpful towards reproducible builds.

Pro: Building the software on your local machine is also relatively easy, as you can use the same Docker image locally.

Pro: Multiple versions of the same package are possible.

Con: You need a Docker file that has all the necessary dependencies preinstalled. For some build environments, a Docker image is available on Docker Hub. For others, it’s not, and you have to write the Dockerfile yourself and possibly create a local Docker repository to store the finished image. This is something you have to learn.

Con: Deploying software might be harder from inside Docker. For this, you have to either setup an SSH user inside the container (which you might not have created yourself), or mount a directory (or volume) on the host to the container. This might depend on the CI software in use, of course.

Enter Nix

There is a third solution: Nix¹. Before we get to the pros and cons, let me explain what it is.

Nix is a package manager, just like dnf, pacman, or apt-get. Unlike those, it’s not bound to a specific distribution². You can install Nix on your machine right now and have two package managers installed! But why would you?

Nix has a lot of cools things in store, but for the sake of this article, I’m going to focus on one feature: with Nix, you can install packages as a normal, unprivileged user. Usually, this would be dangerous. What if a malicious user installs a malicious version of Firefox on your system which sniffs all the passwords you enter? But when you install software with Nix, it’s more like you download the Firefox sources, compile it yourself, and then add your compiled Firefox to PATH . In other words, other users don’t see “your” Firefox version. And actually, that’s close to what really happens³.

Before we get to actually using it, let’s look at configuring Nix in Gitlab CI and Travis.

Nix in Gitlab CI

There are two ways to use Nix from Gitlab CI.

You configure a Docker runner for your project and can then specify a Docker image to use for the CI build using the image directive in your .gitlab-ci.yml file. You configure a shell runner. This will execute the build commands in a normal shell on the machine where you installed gitlab-runner .

If you opt for Docker to build your project (or you’re using Gitlab hosted at gitlab.com), you have to tell it to use a Docker image containing Nix. There an official image available at Docker Hub, so this suffices:

image: nixos/nix:latest

If you opt for a shell runner, you have to install Nix either globally, or just for the gitlab-runner user. A multi-user install is best done with the corresponding distribution package. A single-user install consists of executing a one-liner and following the instructions. Follow “Get Nix” here.

Nix in Travis

With Travis, it’s even simpler, since Travis has Nix support out of the box. All you have to do is add the following to your .travis.yml :

language: nix

Using Nix

Let’s say the instructions to build your software are contained in a small bash script build.sh in your source repository. To start build.sh with the necessary dependencies installed, you can use nix-shell . If you’re building a Java project, for example, you can request a shell with a JDK and Maven installed using⁴: nix-shell -p jdk maven . If you execute this, you get an interactive shell with those two packages in your PATH . Using…

nix-shell -p jdk maven --run ./build.sh

…you can execute build.sh directly.

Ok, but what about reproducible builds? Isn’t this just as susceptible to updates breaking the build as the previous approaches? If you’re doing just what I just described, that’s true. But with a little extra effort, we can do better.

When you install Nix, you typically set a “channel” to take packages from. You can think of a channel as a specific commit in package repository. When you run nix-shell -p jdk , Nix will (hypothetically)…

clone the repository defined in the currently active channel,

checkout the commit defined by the current channel,

find the build instructions for jdk in the cloned repository,

in the cloned repository, build jdk and all its dependencies transitively⁵.

So if you pin (or freeze) the channel in your CI script, your build does not depend on any external library that might get updated or changed. To do that, you can, for example, set the environment variable NIX_PATH to a specific commit, as such:



nix-shell -p jdk maven --run build.sh export NIX_PATH="nixpkgs= https://github.com/NixOS/nixpkgs/archive/ab593d46dc38b9f0f23964120912138c77fa9af4.tar.gz nix-shell -p jdk maven --run build.sh

This not only pins the version of jdk and maven , but of all their dependencies, too. At our company, we’ve set up a CI Variable in Gitlab CI set to the contents of NIX_PATH , so we can easily update the software needed for the build.

The verdict

So, just as before, what are the pros and cons of Nix:

Pro: You can encode the dependencies your software needs inside your source repository and thus tie them together more tightly.

Pro: Reproducible builds are as easy as with Docker.

Pro: You can build your software the exact same way your CI does by installing Nix on your local machine and just running the build commands yourself.

Pro: Nix’s “nixpkgs” software repository is plentiful. You can check if your build and tests tools are available here, but probably, they are. There’s around 42,000 packages in there at the time of writing.

Pro: Nix is easily integrated with at least two major CI systems, as we’ve shown.

Con: Nix is, just as Docker, something you have to learn.

Con: nix-shell will download and provide the software you ask from it. But it will not clean up after you’re done. This is nice, because using nix-shell with the same packages again will not re-download anything. But it also means you hold a cache somewhere that gets bigger over time. You have to take care of that (for example, by regularly calling nix-collect-garbage ).

I hope I could convince you to at least give Nix a try. We’ve been using it with Gitlab CI for a year now, and we’re really happy so far. And I hope the hardcore Nixers aren’t disappointed to find no mention of nix-build and the Nix language here. This is the topic of another article.

If you’re interested in learning Nix, I recommend the excellent Nix Pills series, as well as the Nix manual (describing the package manager) and the nixpkgs manual (describing the package infrastructure).