Hello!

Welcome to my talk on learning Nix the fun way.

This slide deck can be found online at the following link. I welcome improvements, edits and corrections.

Great, so before we begin, let me introduce myself.

My name is Farid Zakaria, and I'm a software engineer at Meta.

I've been using Nix in various capacities for quite a while now. I've presented at a few previous incarnations of Nix conventions, and I've had the pleasure of bringing Nix into many of the companies I've worked at, including Confluent and Google.

I'm very bullish on Nix and it has been a source of mostly joy, and it was additionally the inspiration for a lot of my PhD research.

I have given Nix talks a few times, individually and to groups -- including to engineering groups. I've noticed that explaining the rationale for Nix, and even Nix itself, is challenging. It's an active area of discussion within the community.

Even reflecting on my own journey of learning and loving Nix, it took a few iterations, so I'm pretty empathetic to those that feel they might not understand what the big deal is or find it too complex.

So the disclaimer is that some of what I demonstrate may be complicated or tough to understand. What I do hope to convey however is that there is a reason for this complexity. Software is complex, and aiming for the levels of guarantees that Nix provides puts you face to face with that complexity.

Please feel free to ask me questions during the talk. I have kept the slides light on written text, but there are snapshots of code. I have all of the code handy, and if it's helpful I'm willing to DO IT LIVE and go beyond what is shown.

Okay, so this is the alternate working title for this talk.

I want to share why I love Nix. Nix has brought a lot of joy and simplification to my software life, and oftentimes learning it can feel challenging without much insight into what is the purpose.

I've given a lot of talks about Nix. I've given talks to meetup groups, friends and plenty internally at the companies I've worked at.

This is how I've always pictured myself giving a talk about Nix.

It's always a noble pursuit where I impart the beautiful knowledge of that which is Nix.

** click **

I tend to always begin these talks at the beginning, by discussing Eelco's PhD, The Purely Functional Software Deployment Model, where he first shared Nix with the world in 2006.

I've given a lot of talks about Nix. I've given talks to meetup groups, friends and plenty internally at the companies I've worked at.

This is how I've always pictured myself giving a talk about Nix.

It's always a noble pursuit where I impart the beautiful knowledge of that which is Nix.

** click **

I tend to always begin these talks at the beginning, by discussing Eelco's PhD, The Purely Functional Software Deployment Model, where he first shared Nix with the world in 2006.

In retrospect, this slide better depicts how the talks have gone. People in attendance are at best totally bored by the nuance and complexity of what I'm discussing.

** click **

At worst, I've immediately turned off potential new Nix-enthusiasts by jumping straight into academic and dense vocabulary about the ideas and technology that powers Nix.

In retrospect, this slide better depicts how the talks have gone. People in attendance are at best totally bored by the nuance and complexity of what I'm discussing.

** click **

At worst, I've immediately turned off potential new Nix-enthusiasts by jumping straight into academic and dense vocabulary about the ideas and technology that powers Nix.

The motivation of this talk is to do it different this time.

** click **

I want to impart the benefits of using Nix by what I believe to be learning Nix the fun way.

The motivation of this talk is to do it different this time.

** click **

I want to impart the benefits of using Nix by what I believe to be learning Nix the fun way.

Let's begin with something seemingly innocuous. Here is a small script to determine the IP of your current machine.

This should look pretty standard, I'm hoping, to most people, and probably reminiscent of a lot of scripts we might have written ourselves that stitch a few programs together.

** click **

And it does what it says.

Let's begin with something seemingly innocuous. Here is a small script to determine the IP of your current machine.

This should look pretty standard, I'm hoping, to most people, and probably reminiscent of a lot of scripts we might have written ourselves that stitch a few programs together.

** click **

And it does what it says.

Here is what rewriting that same script in the most raw form of Nix might look like.

I know I just showed you a new language, but for a tiny second just suspend questions you may have. The only take-away from the language would be that it looks semi-similar to other languages you may have used; it has a somewhat JSON-like feel to it.

** click **

Here is the important part of this Nix recipe, and it looks similar-ish to what we had before.

Here is what rewriting that same script in the most raw form of Nix might look like.

I know I just showed you a new language, but for a tiny second just suspend questions you may have. The only take-away from the language would be that it looks semi-similar to other languages you may have used; it has a somewhat JSON-like feel to it.

** click **

Here is the important part of this Nix recipe, and it looks similar-ish to what we had before.

But of course, this is not what you'd actually write in Nix. The following is a little more idiomatic and a lot more terse.

Here it much more resembles the original script.

** click **

For those that might want to follow along now or later, it's important to keep note of this commit value, and that I'm building this on an amd64 Linux machine.

But of course, this is not what you'd actually write in Nix. The following is a little more idiomatic and a lot more terse.

Here it much more resembles the original script.

** click **

For those that might want to follow along now or later, it's important to keep note of this commit value, and that I'm building this on an amd64 Linux machine.

But of course, this is not what you'd actually write in Nix. The following is a little more idiomatic and a lot more terse.

Here it much more resembles the original script.

** click **

For those that might want to follow along now or later, it's important to keep note of this commit value, and that I'm building this on an amd64 Linux machine.

Let's build it!

** click **

We get some output path, okay cool.

** click **

That output path has the script we created.

** click **

We can execute that script and it works as expected.

Let's build it!

** click **

We get some output path, okay cool.

** click **

That output path has the script we created.

** click **

We can execute that script and it works as expected.

Let's build it!

** click **

We get some output path, okay cool.

** click **

That output path has the script we created.

** click **

We can execute that script and it works as expected.

Let's build it!

** click **

We get some output path, okay cool.

** click **

That output path has the script we created.

** click **

We can execute that script and it works as expected.

What is the script we produced though? Why did we need Nix?

** click **

The output of our script looks very similar now to the original variant we had before, except everything seems to be prefixed with some long string.

What is the script we produced though? Why did we need Nix?

** click **

The output of our script looks very similar now to the original variant we had before, except everything seems to be prefixed with some long string.

Although I promised not to go too academic into Nix, this part is important.

** click **

What is that long prefix string ahead of our built script, and similarly found within the script before each of the tools?

** click **

There's a handy Nix command to show us the meaning of that prefix.

Although I promised not to go too academic into Nix, this part is important.

** click **

What is that long prefix string ahead of our built script, and similarly found within the script before each of the tools?

** click **

There's a handy Nix command to show us the meaning of that prefix.

Although I promised not to go too academic into Nix, this part is important.

** click **

What is that long prefix string ahead of our built script, and similarly found within the script before each of the tools?

** click **

There's a handy Nix command to show us the meaning of that prefix.

The whole purpose of Nix is to kind of build this file. This is the recipe describing what it takes to build our IP script.

** click **

It has a build command.

** click **

The text of our script, which may also be the source code if it's a little more involved.

** click **

The tools we depend on to make the script function, like cURL and jq.

** click **

The system we're running on, since Nix also supports different architectures and operating systems.

** click **

All of this information is hashed to calculate this prefix.

There's a really mind-bending property though.

The whole purpose of Nix is to kind of build this file. This is the recipe describing what it takes to build our IP script.

** click **

It has a build command.

** click **

The text of our script, which may also be the source code if it's a little more involved.

** click **

The tools we depend on to make the script function, like cURL and jq.

** click **

The system we're running on, since Nix also supports different architectures and operating systems.

** click **

All of this information is hashed to calculate this prefix.

There's a really mind-bending property though.

The whole purpose of Nix is to kind of build this file. This is the recipe describing what it takes to build our IP script.

** click **

It has a build command.

** click **

The text of our script, which may also be the source code if it's a little more involved.

** click **

The tools we depend on to make the script function, like cURL and jq.

** click **

The system we're running on, since Nix also supports different architectures and operating systems.

** click **

All of this information is hashed to calculate this prefix.

There's a really mind-bending property though.

The whole purpose of Nix is to kind of build this file. This is the recipe describing what it takes to build our IP script.

** click **

It has a build command.

** click **

The text of our script, which may also be the source code if it's a little more involved.

** click **

The tools we depend on to make the script function, like cURL and jq.

** click **

The system we're running on, since Nix also supports different architectures and operating systems.

** click **

All of this information is hashed to calculate this prefix.

There's a really mind-bending property though.

The whole purpose of Nix is to kind of build this file. This is the recipe describing what it takes to build our IP script.

** click **

It has a build command.

** click **

The text of our script, which may also be the source code if it's a little more involved.

** click **

The tools we depend on to make the script function, like cURL and jq.

** click **

The system we're running on, since Nix also supports different architectures and operating systems.

** click **

All of this information is hashed to calculate this prefix.

There's a really mind-bending property though.

The whole purpose of Nix is to kind of build this file. This is the recipe describing what it takes to build our IP script.

** click **

It has a build command.

** click **

The text of our script, which may also be the source code if it's a little more involved.

** click **

The tools we depend on to make the script function, like cURL and jq.

** click **

The system we're running on, since Nix also supports different architectures and operating systems.

** click **

All of this information is hashed to calculate this prefix.

There's a really mind-bending property though.

That recipe earlier contained the tools we needed, and the source code; each one also had a unique hash prefix.

There's a level of recursion here that means repeatedly seeing this hash is an incredibly strong property of reproducibility.

It means not only was our script text the same and tool dependencies the same, but the same applies to each tool itself and so forth.

In Nix, you spend considerable time wiring things up, and the purpose is to completely model your software as a graph.

Turns out that once you have a graph, everything you might want to do in software is a permutation or transformation on the graph.

Here we have the full dependency graph of our seemingly innocuous script. It's a little more involved than originally thought.

Okay, here is another Nix snippet. Again ignore the actual syntax, but know that what we are defining here is a shell environment.

** click **

We import our same script and add it to the packages that we want present in our shell.

Okay, here is another Nix snippet. Again ignore the actual syntax, but know that what we are defining here is a shell environment.

** click **

We import our same script and add it to the packages that we want present in our shell.

Okay, here is another Nix snippet. Again ignore the actual syntax, but know that what we are defining here is a shell environment.

** click **

We import our same script and add it to the packages that we want present in our shell.

Let's enter the shell.

** click **

We see the shell hook we wrote. That's just an aid to demonstrate we entered the shell.

** click ** ** click **

My PATH now contains what-is-my-ip, and importantly at the exact same hash prefix string we've built before.

** click **

The script also works and runs as we expect.

Let's enter the shell.

** click **

We see the shell hook we wrote. That's just an aid to demonstrate we entered the shell.

** click ** ** click **

My PATH now contains what-is-my-ip, and importantly at the exact same hash prefix string we've built before.

** click **

The script also works and runs as we expect.

Let's enter the shell.

** click **

We see the shell hook we wrote. That's just an aid to demonstrate we entered the shell.

** click ** ** click **

My PATH now contains what-is-my-ip, and importantly at the exact same hash prefix string we've built before.

** click **

The script also works and runs as we expect.

Let's enter the shell.

** click **

We see the shell hook we wrote. That's just an aid to demonstrate we entered the shell.

** click ** ** click **

My PATH now contains what-is-my-ip, and importantly at the exact same hash prefix string we've built before.

** click **

The script also works and runs as we expect.

Let's enter the shell.

** click **

We see the shell hook we wrote. That's just an aid to demonstrate we entered the shell.

** click ** ** click **

My PATH now contains what-is-my-ip, and importantly at the exact same hash prefix string we've built before.

** click **

The script also works and runs as we expect.

Wow. Do we all realize what we just did?

We just created a simple way to bring reproducible developer environments to projects.

No need for containers. No need for namespaces. Easy to enter and easy to validate thanks to that prefix hash.

We actually know everything about our code now.

** click **

Because we know the full dependency closure, we can do binary or source deployments.

** click **

We simply copy the necessary store paths to another machine...

** click **

...and that machine doesn't even need Nix installed.

** click **

Profit.

We actually know everything about our code now.

** click **

Because we know the full dependency closure, we can do binary or source deployments.

** click **

We simply copy the necessary store paths to another machine...

** click **

...and that machine doesn't even need Nix installed.

** click **

Profit.

We actually know everything about our code now.

** click **

Because we know the full dependency closure, we can do binary or source deployments.

** click **

We simply copy the necessary store paths to another machine...

** click **

...and that machine doesn't even need Nix installed.

** click **

Profit.

We actually know everything about our code now.

** click **

Because we know the full dependency closure, we can do binary or source deployments.

** click **

We simply copy the necessary store paths to another machine...

** click **

...and that machine doesn't even need Nix installed.

** click **

Profit.

We actually know everything about our code now.

** click **

Because we know the full dependency closure, we can do binary or source deployments.

** click **

We simply copy the necessary store paths to another machine...

** click **

...and that machine doesn't even need Nix installed.

** click **

Profit.

Here is one such command to copy the graph we saw earlier to another machine. In this example we are leveraging a nix copy command, but you could do something as dumb as scp or rsync.

** click **

The items are copied over. Now let's login to the machine.

** click **

The exact store path we want is present there and runs. The fact that it runs means all of its dependencies, who also have hash prefix strings, are also present.

Here is one such command to copy the graph we saw earlier to another machine. In this example we are leveraging a nix copy command, but you could do something as dumb as scp or rsync.

** click **

The items are copied over. Now let's login to the machine.

** click **

The exact store path we want is present there and runs. The fact that it runs means all of its dependencies, who also have hash prefix strings, are also present.

Here is one such command to copy the graph we saw earlier to another machine. In this example we are leveraging a nix copy command, but you could do something as dumb as scp or rsync.

** click **

The items are copied over. Now let's login to the machine.

** click **

The exact store path we want is present there and runs. The fact that it runs means all of its dependencies, who also have hash prefix strings, are also present.

Wow. We just did a deployment of not only our "code", which was our script, but all of its necessary dependencies.

This blows my mind. I can't tell you the number of times in practice I had to coordinate the runtime needed on the host or some other base library being present.

Well, maybe that's a bit aggressive for deployments, and you have already drunk the kool-aid of Kubernetes.

** click **

No problem! Since we know the graph, everything we'd want is merely a permutation on our graph.

Here is another snippet, and again ignore the syntax, but know this is creating an OCI compliant image.

** click **

We set the command of the image to be our what-is-my-ip package.

Well, maybe that's a bit aggressive for deployments, and you have already drunk the kool-aid of Kubernetes.

** click **

No problem! Since we know the graph, everything we'd want is merely a permutation on our graph.

Here is another snippet, and again ignore the syntax, but know this is creating an OCI compliant image.

** click **

We set the command of the image to be our what-is-my-ip package.

Well, maybe that's a bit aggressive for deployments, and you have already drunk the kool-aid of Kubernetes.

** click **

No problem! Since we know the graph, everything we'd want is merely a permutation on our graph.

Here is another snippet, and again ignore the syntax, but know this is creating an OCI compliant image.

** click **

We set the command of the image to be our what-is-my-ip package.

Let's build the image.

** click **

The output is an OCI tarball.

** click **

We can load that tarball into the docker daemon as normal.

** click **

Running the image works as we'd expect as well.

** click **

Inspecting the image, we see yet again that same prefix hash on our package for the command to execute -- which continues to give us comfort about the version and state of our software.

Let's build the image.

** click **

The output is an OCI tarball.

** click **

We can load that tarball into the docker daemon as normal.

** click **

Running the image works as we'd expect as well.

** click **

Inspecting the image, we see yet again that same prefix hash on our package for the command to execute -- which continues to give us comfort about the version and state of our software.

Let's build the image.

** click **

The output is an OCI tarball.

** click **

We can load that tarball into the docker daemon as normal.

** click **

Running the image works as we'd expect as well.

** click **

Inspecting the image, we see yet again that same prefix hash on our package for the command to execute -- which continues to give us comfort about the version and state of our software.

Let's build the image.

** click **

The output is an OCI tarball.

** click **

We can load that tarball into the docker daemon as normal.

** click **

Running the image works as we'd expect as well.

** click **

Inspecting the image, we see yet again that same prefix hash on our package for the command to execute -- which continues to give us comfort about the version and state of our software.

Let's build the image.

** click **

The output is an OCI tarball.

** click **

We can load that tarball into the docker daemon as normal.

** click **

Running the image works as we'd expect as well.

** click **

Inspecting the image, we see yet again that same prefix hash on our package for the command to execute -- which continues to give us comfort about the version and state of our software.

Nix and Docker integrate perfectly. The image has exactly the files necessary to run the tool -- effectively distroless.

And the same trick works for any deployment format: tarballs, RPMs, debs, AppImages. They are all just another permutation of the graph.

Which brings us to the big one. A Linux distribution is simply a collection of packages.

Here is a whole NixOS machine described in Nix.

** click **

Our same script, added to the packages for a user named alice.

** click **

And we ask for it as a virtual machine.

Here is a whole NixOS machine described in Nix.

** click **

Our same script, added to the packages for a user named alice.

** click **

And we ask for it as a virtual machine.

Here is a whole NixOS machine described in Nix.

** click **

Our same script, added to the packages for a user named alice.

** click **

And we ask for it as a virtual machine.

Here is a whole NixOS machine described in Nix.

** click **

Our same script, added to the packages for a user named alice.

** click **

And we ask for it as a virtual machine.

Build it.

** click **

A VM.

** click ** ** click **

Boot it, and log in as alice.

** click **

Our script is on alice's PATH, it resolves to the very same store path, and it runs.

Build it.

** click **

A VM.

** click ** ** click **

Boot it, and log in as alice.

** click **

Our script is on alice's PATH, it resolves to the very same store path, and it runs.

Build it.

** click **

A VM.

** click ** ** click **

Boot it, and log in as alice.

** click **

Our script is on alice's PATH, it resolves to the very same store path, and it runs.

Build it.

** click **

A VM.

** click ** ** click **

Boot it, and log in as alice.

** click **

Our script is on alice's PATH, it resolves to the very same store path, and it runs.

Build it.

** click **

A VM.

** click ** ** click **

Boot it, and log in as alice.

** click **

Our script is on alice's PATH, it resolves to the very same store path, and it runs.

We've now taken one tiny script and turned it into a developer shell, a deployment, a container and an operating system. So let's step back: what actually is Nix?

Nix is everything, which is part of why it's hard to explain. It began as Eelco Dolstra's PhD research, and I highly recommend the read.

It's a language, depicted here. It's used ultimately to create what the language refers to as 'derivations', but you can think of them as build recipes. The language resembles JSON a little, with the added bonus that it's a pure, functional, lazy, untyped language. It's kind of like a poor version of Haskell.

It's also the command line tool itself, called nix, with which I can do a bunch of stuff like build these recipes or create developer environments, as we saw.

It's a package collection, known as nixpkgs. On repology's map of every package repository, nixpkgs is out on its own: the largest, and among the freshest.

GitHub publishes a yearly report known as Octoverse, and nixpkgs has been in the top 10 across multiple dimensions such as contributors, activity and pull requests.

Another fun fact: this whole package set can be bootstrapped from a 357-byte binary seed.

Because nixpkgs is a graph, a package is not one thing, it's a recipe you can permute.

** click **

Here's GNU hello, built normally.

** click **

The same hello, statically linked against musl. One attribute.

** click **

Cross-compiled for RISC-V. It won't run on my x86 laptop, which is the point.

** click **

Or for Windows, and we can run it under wine. Same recipe, every time; just a different path through the graph.

Because nixpkgs is a graph, a package is not one thing, it's a recipe you can permute.

** click **

Here's GNU hello, built normally.

** click **

The same hello, statically linked against musl. One attribute.

** click **

Cross-compiled for RISC-V. It won't run on my x86 laptop, which is the point.

** click **

Or for Windows, and we can run it under wine. Same recipe, every time; just a different path through the graph.

Because nixpkgs is a graph, a package is not one thing, it's a recipe you can permute.

** click **

Here's GNU hello, built normally.

** click **

The same hello, statically linked against musl. One attribute.

** click **

Cross-compiled for RISC-V. It won't run on my x86 laptop, which is the point.

** click **

Or for Windows, and we can run it under wine. Same recipe, every time; just a different path through the graph.

Because nixpkgs is a graph, a package is not one thing, it's a recipe you can permute.

** click **

Here's GNU hello, built normally.

** click **

The same hello, statically linked against musl. One attribute.

** click **

Cross-compiled for RISC-V. It won't run on my x86 laptop, which is the point.

** click **

Or for Windows, and we can run it under wine. Same recipe, every time; just a different path through the graph.

Because nixpkgs is a graph, a package is not one thing, it's a recipe you can permute.

** click **

Here's GNU hello, built normally.

** click **

The same hello, statically linked against musl. One attribute.

** click **

Cross-compiled for RISC-V. It won't run on my x86 laptop, which is the point.

** click **

Or for Windows, and we can run it under wine. Same recipe, every time; just a different path through the graph.

It's a store. This is actually a very important distinction that separates Nix from traditional software installations: it forgoes the Filesystem Hierarchy Standard, the well-known directories like /lib and /etc that we all know and love.

By forgoing that limitation, Nix is capable of having limitless variants of a single package, since they are no longer confined to a single key space.

Furthermore, dependencies of Nix software only exist within other store entries, forming a DAG.

It's an operating system. Once you have solved producing software packages, a Linux distribution is merely a collection of software packages and a kernel.

Fundamentally though, Nix is the idea of modeling all software as a directed acyclic graph with an immense level of fidelity.

It's an idea that says: what if every part of our build system was accounted for?

The language, the store, the package set, the OS, the dev shells -- they're all just ways of building, storing and walking that graph.

Which is why the naming is so confusing. Nix the language, Nix the package manager, and NixOS the operating system are all called Nix, but none of them is the other. What they share is the graph.

For the last part, let's go one level down and look at the mechanism.

I'm going to go over very briefly how software is laid out traditionally, hopefully illuminate how it's utter madness, and then tie the thread to see how Nix solves it.

A recurring theme: Docker, specifically containers, are not a solution to any of this. Containers are just a snapshot at a particular state.

I'm going to pick Ruby, as it's an innocuous piece of software. It's written in C, present on every distribution, and would seemingly have a very small set of dependencies.

Here are the Rubies on a typical machine.

** click **

Which one is this? What was it built with?

I'm going to pick Ruby, as it's an innocuous piece of software. It's written in C, present on every distribution, and would seemingly have a very small set of dependencies.

Here are the Rubies on a typical machine.

** click **

Which one is this? What was it built with?

Ruby's own build instructions. A C compiler, OpenSSL, libyaml, zlib -- and if you build from git, autoconf, gperf, and a Ruby to build Ruby. Plus optional extras.

Where does each of these come from on your machine? What version?

Here is what the installed ruby resolves at runtime. Everything comes from one shared /lib, by name only. Whatever libz.so.1 happens to be there, wins.

And with the full verbose tree, every library pulls in more libraries, all resolved the same way: by name, from a single global directory.

And this is Ruby's full build graph in nixpkgs, every input accounted for. This is the thing Nix makes you model -- and once you have it, everything else we saw today falls out of it.

A quick primer on what a binary is in Linux. All binaries on Linux are in the ELF format. It's an incredibly generic format.

Specifically, a dynamic binary must have an interpreter set, which is always the dynamic linker, and can list the libraries it needs. Note that NEEDED entries are just names -- not paths, and not versions.

This slide won't do the concept justice at all, so I apologize; I could probably spend multiple hours talking about it.

In order to add reproducibility to the world of software management, a new concept was developed in the early 2000s as part of a PhD thesis. The idea was to nest all software underneath a single root, include a cryptographic hash in the name so similar packages could live side by side, and restrict all dependencies to other entries in the root, or store.

Dynamic linking relies on a feature to specify an RPATH on the binary, which supersedes the FHS search. Each NEEDED name now resolves against exact store paths.

Similar to RPATH, the developer environments set the PATH variable to isolate access to the current terminal.

And that's how it feels once it clicks.

Thank you! The slides and every example are online; questions welcome.