raccoon's zone!

This web is still being woven. Please don't be surprised if it changes from time to time...

last updated 2024-01-29 for nix 2.13 - 2.18, nixos 23.05 - 23.11

Introduction to Nix

Hello! This is my guide to understanding the build automation software/package manager called Nix, and the Linux distribution built on top of it called NixOS. I hope it helps :)

If you have any questions, complaints, or suggestions, contact us!


You'll need to have Nix on your system to follow along with some of the demonstrations. You can still read this document without a Nix install, but you might find it helpful to observe its behavior yourself.

We won't be covering all of the features of every command we use, so if at any point you want more information, you can pass --help to any command or subcommand you want to use, or consult the Nix manual.

If you don't already have Nix installed, you can download it here. The option of a "multi-user" or "single-user" install doesn't matter for the purposes of this guide, so feel free to follow the directions there and decide what works for your system.

At the time of writing, Nix's new commandline interface introduced in Nix 2.0 is still marked experimental. Since its design is more intuitive than its predecessor and will eventually replace it, we're going to be using it anyway. Be aware that some details may have changed since the version this guide was written for.

To enable the features we need, you should modify the Nix config file at ~/.config/nix/nix.conf, creating it if it does not exist, and add the following line:

experimental-features = nix-command flakes

What is Nix?

1. A content store

The Nix store (default /nix/store/) is a repository of files and directories ("objects") each identified by a unique name and hash. The simplest way to interact with a store is to add a fixed object, hashed by its content.

$ echo "Hello, world!" >hello
$ nix store add-path hello

Any Nix store, given this file with this name and content, will place it at the same store path you see there. As a result, all Nix stores that you trust constitute a big distributed cache you can freely copy objects between!

$ HELLO=/nix/store/rw26ab68fybv6fyycyz61k7rvdgqgv06-hello

# give the system at example.org my hello file
$ nix copy --to ssh://example.org $HELLO

# and now i can delete it locally
$ nix store delete $HELLO
1 store paths deleted, 0.00 MiB freed

# because i can get it back whenever i want :)
$ nix copy --from ssh://example.org $HELLO

2. A reproducible build system

The property that store paths uniquely identify their objects has further implications once you introduce the derivation, a special store object that represents a build recipe. That is, a function that consumes existing objects as input and produces new objects as output.

Derivations are forced to identify all their inputs as store objects, so they will always be given the exact specified versions of the particular resources they need, and nothing more. We can assume a derivation with a particular store path has enough information to reproduce the same build every time on any system of the same platform.

Given this assumption, we can also decide that hashes for output objects are based on the derivation that created them, and thus refer to a derivation's output paths before we actually build them.* This results in two important properties:

In general, Nix is aware of object dependency relations, not just for derivations' dependencies but for outputs' too. Nix will copy not just the output object but the set of every object that its contents refer to (its closure, in Nix terminology).

Naturally, if you try to nix store delete any object that has dependents (or, "referrers"), Nix will refuse to do so.

One thing I want to emphasize here is that store objects have no notion of a "system package". Conflicting dependencies are unrepresentable, because each program specifies exactly which object they want. Nix doesn't care that your store might have both 123...-libfoo and abc...-libfoo, dependents will specify the full path anyway.

The same goes for user programs, of course: there's nothing wrong with one user's imp...-python3-3.12.1 existing on the same system as another user's 3iy...-python3-3.13.0a2, as neither will be forced to take the role of the global /usr/bin/python.

We'll talk about producing our own derivations in a supplementary document after this one. For now, let's get into the main purpose for Nix as an end user.

3. A source-based package manager

Nix comes with several built-in conveniences and abstrations to make it more suitable for conventional package management. To start with, Nix knows about remote package repositories, and how to build derivations from them:

nix build nixpkgs#firefox

The argument nixpkgs#firefox is a reference to something that can be built, called an installable, which we'll discuss in detail soon. Abstractly, it's pretty simple: we're asking for the derivation called firefox that lives inside the Nix project (or, "flake") called nixpkgs.

You can search Nixpkgs for more packages on search.nixos.org, or you can use nix search nixpkgs <query> (but that command needs to evaluate all of Nixpkgs locally, so it can be slow)

When you run that command, it probably won't actually build anything! As previously discussed, Nix can substitute builds with copied outputs from other stores, and Nix is configured with

substituters = https://cache.nixos.org

by default. This allows every unmodified package in Nixpkgs to be substituted automatically.

Build commands are also idempotent, so it's safe to run them just to "make sure" an output exists in the store. If it's already there, nothing will happen.

So, regardless of whether your system actually needs to build the specified derivation or not, now we know that there's a path in your Nix store that has Firefox in it. By default, nix build will additionally plop a symlink called result in your current directory, which leads to the relevant store path.

Nix has the capability to delete outputs that are no longer being used in order to save space (nix store gc), and many systems are configured to do this automatically on a timer.

The garbage collector will never delete:

  • direct GC roots (store paths symlinked in /nix/var/nix/gcroots or /nix/var/nix/profiles)
  • runtime roots (store paths in working directories, file descriptors, or environments of running processes)
  • any store object that refers to any of the above at runtime, recursively

So, an additional symlink to your result is placed in
/nix/var/nix/gcroots/auto so that the garbage collector can't pull the thing you just built out from under you until you delete result. That's why this particular build command chooses to communicate its output directory with a symlink.

Nix also comes with some other build commands that make it more convenient to use with runnable programs.

nix run finds the main executable inside the derivation output and runs it:

$ nix run nixpkgs#git -- --version
git version 2.42.0

nix shell opens up a subshell with a PATH that includes all the bin directories from the derivation(s) you specify:

$ nix shell nixpkgs#cowsay nixpkgs#fortune
$ fortune | cowsay
< You do not have mail. >
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
$ exit
$ fortune | cowsay
bash: cowsay: No such file or directory
bash: fortune: No such file or directory

or, if you really want the package in your user's PATH persistently, like a more traditional package manager, you can use Nix's profile system.


Profiles are a way to merge several derivations' outputs one-by-one into a single store path. This offers a more familiar package management interface:

# the same build of nixpkgs#firefox will remain in your profile permanently after install
$ nix profile install nixpkgs#firefox

# until you decide to replace it with a new version
$ nix profile upgrade nixpkgs#firefox

# or remove it entirely
$ nix profile  remove nixpkgs#firefox

By default, your user's profile is linked at ~/.nix-profile, and its bin subdirectory is placed in your PATH. This makes it easy to persistently install software you want to be available all the time, like with a traditional package manager.

Profile commands work on your user profile by default, but you can also specify one explicitly (e.g. --profile /nix/var/nix/profiles/per-user/<username>/profile) to reference other users' profiles, or profiles created for other purposes.

Since store objects can't be modified directly, a profile is actually a hive of symlinks, each to different "generations" of its output. You can see what you changed each time with profile history, and you can profile rollback --to <version> to revert to a specific generation at any time.

Using profiles as a versioning mechanism carries over to other Nix-based projects, too, like NixOS systems!

nix profile rollback --to XXX --profile /nix/var/nix/profiles/system

The idea is that sometimes destructively modifying the state of your environment is necessary or convenient, but these modifications should be atomic, reversible transactions so that you can always get back to a known-good state.

Flakes and Installables

As mentioned before, that nixpkgs#foo string you give to Nix is an example of an installable, a reference to something that can be built by Nix. Specifically, it's a reference to some attribute foo inside the flake called nixpkgs.

A flake is like a "project file" for Nix. It's a way to represent a collection of Nix code that provides certain well-defined outputs, like a package repository, or a Nix library, or a set of NixOS configurations. The Nixpkgs repo, for example, uses its flake to expose the derivations for its software packages so you can reference them by name on the commandline, as we've already seen.

Flake outputs can depend on a set of inputs in the form of other, independently-controlled flakes. For the sake of reproducibility, exact revisions of these inputs are saved in a lock file, with a hash of their contents. That way, a given revision of a flake consistently uses the intended revisions for all its input flakes.

The flake reference nixpkgs is not just a hardcoded value. It's really just an alias in a flake registry, which we can look at with nix registry list. There you'll find a global mapping to the rolling-release Nixpkgs branch on GitHub:

global flake:nixpkgs github:NixOS/nixpkgs/nixpkgs-unstable

This means that wherever you typed nixpkgs, you could type github:NixOS/nixpkgs/nixpkgs-unstable instead.

If you wanted to use, say, the Nixpkgs revision for a particular NixOS release, you can specify a different branch while still using the registry alias for convenience, like nixpkgs/nixos-21.11. This works for other registry aliases with optional parameters, too.

The default global flake registry is downloaded from a file managed by the NixOS project. However, you can override its values and add new ones with alternative mappings:

nix registry add nixpkgs ~/my-nixpkgs-fork
nix registry pin nixpkgs (pins nixpkgs to the exact revision it's at)
nix registry remove nixpkgs

Without specifying a file with --registry, these commands will, by default, add or remove mappings in the user registry located at ~/.config/nix/registry.json. There's also a lower priority system registry at /etc/nix/registry.json, which is intended to be auto-generated from the system configuration on NixOS.

Any directory (but usually a Git repository) containing a special Nix file called flake.nix is recognised as a flake. As we've seen, flakes can be referenced using a name in the registry, a local file path, or through special URL-like syntaxes like github:<user>/<repo>[/branch]. You can find a full list of these reference syntaxes in the manual.

In addition to specifying an installable from a flake reference, you can also use an already existing path in the store (/nix/store/...), or an attribute in a non-flake Nix file with

--file /path/to/file.nix <attr>,

or the result of a Nix expression with

--expr '...'.

The Nix Language

Derivations, flakes, and various other bits of Nix-related configuration are expressed in a pure functional DSL called the Nix language. An overview of the language's syntax follows.

You can follow along in nix repl, if you'd like! In the repl, you can define variables imperatively like x = 1234 for convenience, but be aware that this doesn't exist in Nix code proper: every Nix file is a single expression; no statements.


## strings
"hello, world!"

# with interpolation
"the result is ${some-string}"

# if you want to escape a literal "${", you can prefix it with two single quotes
"''${this is not interpolated}"

# two single quotes also mark a multiline string
line 1
line 2
# equivalent to "line 1\nline 2\n"
# (note that a single leading newline is ignored, but a trailing one is not)

## integers and floats

## booleans and null

## lists
# space-seperated values!
[ 1 2.0 "three" ]

## attribute sets (associative arrays)
  a = 1;
  b = 2.0;
  c = "three";

# shorthand equivalent to { a = { b = { c = 1; }; }; }
{ a.b.c = 1; }

  "blah..." = 1; # attribute names can be quoted if they have special characters
  ${foo} = 2; # and they can be interpolated from other values

# `rec` allows attributes to be defined in terms of other attributes in the same set
# (it's short for 'recursive')
rec {
  a = 4;
  b = a * 2;

## filesystem paths
./relative/${path}/to/file # interpolation works in paths, too
./. # this refers to the current directory; `.` and `./` are not valid path syntax

# if a path is in angle brackets, the starting directory is found in the
# environment variable `NIX_PATH`, a space-seperated list.
# since this functionality is "impure", it isn't accessible in all contexts,
# and you may need to add `--impure` if you ever use it in a Nix command.

# if `NIX_PATH="foo=/path/to/dir"`, then
# becomes

# if `NIX_PATH=/path/to/dir`, all of the subdirectories in `/path/to/dir`
# are considered.
# so, if `/path/to/dir` contains the subdirectory `foo`, then
# becomes

# if not otherwise defined, nix behaves as if
# `NIX_PATH="~/.nix-defexpr/channels /nix/var/nix/profiles/per-user/root/channels"`.
# "channels" are a concept from pre-2.0 made redundant by flakes. all you need to know
# is that some Nix code in the wild will rely on this behavior to refer to a checkout
# of the nixpkgs repo:

## functions
# this is a function that takes an argument `n` and returns `n + 1`
n: n + 1

# functions are called with simple juxtaposition, like `f x`
(n: n + 1) 2 # = 3

# they only take one argument, but currying works
(a: b: a + b) 2 4 # = 6

# or, you can accept a set argument and use a pattern matching syntax to destructure it
({ a, b }: a + b) { a = 2; b = 3; } # = 5

# when you're destructuring a set argument, you can also use @ to bind a name
# to the set as a whole
  # and ? to specify defaults for optional attributes
  optional ? 2,
  # and ... to allow additional, unspecified attributes to be present in the set
}: normal + optional

## derivations
# derivations are a subtype of attribute set ({ type = "derivation"; /*...*/ }), but
# they're special-cased by the Nix machinery enough to sort-of call them their own type.
# there's too much to talk about here; they'll have their own section linked at the end!
derivation { /*...*/ }


In order of precedence!

## accessing an attribute in a set
# fallible access!
set.attr or "default"

## applying a function
f x

## negating a number

## checking for the existence of an attribute
# returns `true` if `set` contains an attribute `x`
set ? x

## concatenating lists
listA ++ listB

## doing arithmetic
a * b
a / b

a + b
a - b

## concatenating strings and paths
stringOrPathA + stringOrPathB

## negating booleans

## merging two sets into one
setA // setB

# if both sets have an attribute with the same name, the one from the second set is used
{ a = 1; b = 2; } // { b = 3; c = 4; } # = { a = 1; b = 3; c = 4; }

# the merge is shallow with regard to nested sets
({ inner = { a = 1; }; } // { inner = { b = 2; }; }) # = { inner = { b = 2; }; }

## doing comparison
a <  b
a <= b
a >  b
a >= b

a == b
a != b

## doing boolean logic
p && q
p || q

# "p implies q": this is `true` in all cases except when `p` is true and `q` isn't
p -> q

Other Language Constructs

## if expression
# (the else branch is required)
if a then b else c

## binding values to use in an expression
  a = 1;
  b = 2;
in a + b

## binding all the attributes in a set
with {
  a = 1;
  b = 2;
}; a + b

## inheriting bound values as attributes in a set
  a = 1;
  b = 2;
  set = { c = 3; };
in {
  inherit a b;
  inherit (set) c;
} # = { a = 1; b = 2; c = 3; }

## asserting that a precondition is true
assert true; x # = x
assert false; x # aborts with error "assertion 'false' failed"

# it's common to use the -> operator to express dependency assertions
# e.g. when creating derivations in Nix code
assert enableFoo -> fooPackage != null; /*...*/

## importing other Nix expressions
# (this is actually just a builtin function, but it's a very special one)
  x = import ./the-value-of-x.nix;

  # if you try to import a directory, Nix will instead import the file named
  # `default.nix` inside that directory
  default-nix = import ./.;

  # if you try to import a set, Nix will try to import the attribute `outPath`,
  # if present, and fail otherwise
  # this is meant to be used with derivations, to import their output paths.
  from-set = import { outPath = ./.; }
  from-drv = import (derivation { /*...*/ })

String Conversion

In the process of building derivations, Nix regularly needs to communicate its values to an outside environment through commandline arguments and environment variables. As such, the way the language converts different values to strings is significant enough to get its own section.

## conversions when using string interpolation
# strings don't need converting
"${"a string"}" # = "a string"

# sets can only be interpolated if they contain a conversion function called `__toString`
  foo = "bar";
  __toString = self: self.foo;
}}" # = "bar"

# or, alternatively, if they contain an attribute `outPath`, this is used.
# again, this is intended for printing derivations' output paths
"${my-drv}" # = "/nix/store/..."

# paths are *copied to the store* as if by `nix add-path` and then that store path is used
"${/path/to/foo}" # = "/nix/store/<hash>-foo"

# integers, floats, lists, functions, booleans, and null can't be interpolated

## conversions when using the builtin `toString`
# the same as string interpolation, except:

# paths aren't copied
toString /path/to/foo # = "/path/to/foo"
# but relative paths are displayed in absolute form
toString ./foo # = "/path/to/foo" (if your current directory is /path/to)

# integers are displayed as written
toString 10 # = "10"

# floats are displayed with exactly 6 digits after the point, subject to rounding
toString 1.0 # = "1.000000"
toString 1.1234561 # = "1.123456"
toString 1.9999996 # = "2.000000"

# true, false, and null are weird
toString true # = "1"
toString false # = ""
toString null # = ""

# lists are flattened, then each value is converted and joined with spaces
toString [ 3 [ "different" { __toString = _: "types" } ] ] # = "3 different types"

A number of builtin functions not mentioned here are also included. Most of them are inside a set called builtins, but some of the most important ones are also available in the global namespace directly. You can find a complete listing in the Nix manual.

Perhaps the most important builtin we haven't talked about is derivation, for producing a derivation value from inside the Nix language.* There's a lot to cover about writing your own derivations, though, enough to deserve its own page!

Writing Packages

Or, if you're more interested in configuring NixOS systems or declaratively managing your dotfiles, you can start here instead!

NixOS Modules