WelcomeNixLanguageExpressionsNumbersArithmeticStringsIndented stringsBooleansComparisonsBoolean operatorsif … then … elsenullListsJoining lists with ++Attribute setsSelecting attributesFallbacks with orChecking with ?Merging sets with //reclet … inString interpolationwithinheritFunctionsMultiple argumentsSet argumentsDefault argumentsExtra arguments with ...Naming arguments with @builtinsTypesmapLazinessthrow and assertInfinite recursionPathsimportNixpkgspkgsPackage attributeslibcallPackageoverrideoverrideAttrsOverlaysOverlays in NixOS and flakesThe Nix storeStore pathsDerivationsYour first packageC projectsPython projectsRust projectsSharing your packageThe default packagePackage your projectBuild phasesbuildInputs and nativeBuildInputsHooks and runHookFetchersChoosing source filesFrom your project to nixpkgsThe build sandboxDerivations in stringsClosuresGarbage collectionBinary cachesWritersWrapping programsWrapping any packageConfigs inside a wrappernix-wrapper-modulesInstalling NixLegacy and experimental commandsnix-buildnix-shellnix replExperimental featuresnix evalnix buildnix shellnix runNixOSconfiguration.nixnixos-rebuildGenerationsenvironment.systemPackagesUsersServiceshardware-configuration.nixNixOS from a flakeSeveral machines in one flakePassing inputs to modulesGraphics driversDesktops and window managersInstall NixOSConfigure NixOSTurn it into a flakeModulesOptionsThe config argumentimportsMerging definitionsmkDefault and mkForcemkIfConditional imports and infinite recursionDeclaring optionsOption typesHome Managerprogramshome.fileHome Manager inside NixOSNixOS settings in Home ManagerInstall Home ManagerTake your home with youFlakesFlake referencesInstallablesfollowsPackages from a second nixpkgsflake-partsSystemsFlakes only see files in GitPut it in GitmkShelldevShellsnix developdirenvdevenvCreate a devshell for your project
Nix builds things from a description. You write down what you want, and Nix
makes exactly that.
That’s not how we usually work with a computer. Normally, you perform actions
one by one: install a browser, name the machine, turn on a service. It works
fine, until you get a second machine and have to remember every command you ran.
$$$$
With Nix, you write the result down instead. Here’s that same setup on
:
Now the file remembers it all for you. Copy it to a new machine, and you get
the same setup back.
configuration.nix
That’s what means. You describe the
result, and Nix works out how to get there.
And it’s not just operating systems. With , you
can set up a development environment for your project the same way:
devenv.nixdevenvA devenv module.
And with , your
:
home.nixexcerptHome ManagerA Home Manager module.
Different tools, same language. That’s why most roads on this map start with
it.
An is a piece of Nix code that gives back a
. 1+2 is an expression, and its value is 3:
Nix code is made of nothing but expressions. A number, a , a , and
even a whole NixOS file are each one expression. Some are just a
lot bigger than others.
Bigger expressions are built from smaller ones. In 1+2*3, the 2*3 is
an expression of its own. Nix works it out first, and then adds it to 1:
Numbers in Nix look the way you’d write them anywhere. Type one, and you get it
right back:
Whole numbers like 42 are called . A number with a dot, like 0.5,
is called a :
Numbers can be negative too. Put a minus in front:
Try it
Put a minus in front of 42.
Math in Nix works just like it did in school. We have +, -, * and /,
and * and / go first:
Want a different order? Wrap a part in ,
and Nix works it out first:
Dividing two gives back an integer, so the fraction is
simply dropped. Make either number a , and it stays:
Change 7 to 7.0, so the result keeps its fraction.
Also put spaces around /. Without them, Nix thinks 6/3 is a to a file,
and gives you that path instead of 2:
Try it
Put spaces around /, so Nix divides 6 by 3.
Some text in double quotes is a staple in any modern programming language.
in Nix for the most part work exactly as you would expect them to:
To join strings together, simply put a + between them:
Notice that + doesn’t add a space for you. If you want one, join it in as a
string of its own:
But what if your text has a double quote inside? Nix thinks the string ends
right before hi, so it has no idea what hi is:
To fix that, put a backslash in front of each quote inside. It tells Nix that
this quote is just part of the text:
Try it
Put escaped quotes around hi, so it says "say \"hi\"".
Plenty of Nix code is really text in another language, like a script or a
config file. Writing that in a normal gets messy
fast. To keep your code indented, you end up writing every line break as \n:
So instead, Nix gives us . Simply wrap the text in two single
quotes on each end, and the line breaks stay just as you typed them:
That’s the exact same string as above.
And the spaces in front of each line aren’t part of the text. Nix
removes the indentation all the lines share, so you can indent the text along
with your code.
Double quotes need no backslash in there either, which is very handy for
scripts:
The prints every result as a normal string, so the backslashes come back
in its output.
You’ll see indented strings all over , in pretty much every build script
and config file a writes.
Try it
Indent line two by two more spaces than line one, and see those two spaces stay in the result.
A is simply a yes or a no. Nix writes them as
true and false, and that’s all there is to it:
You’ll see them all over Nix, because almost anything you can switch on or off
takes one. Your NixOS will be full of lines like enable=true;.
Notice that both words are lowercase. With a capital letter, Nix doesn’t see a
boolean at all, and goes looking for something called True instead:
And they go without quotes, because "true" is just a that happens to
spell the word.
Try it
Switch this false to the other boolean.
Is 22 smaller than 1024? Nix answers questions like that with a comparison, and
the answer is always a :
Nix has all the usual ones: <, <=, >, >=, == for equal, and != for
not equal. Each side can be any , so you can even check your math:
Numbers compare by their amount, so an and a can be equal:
Comparisons don’t chain though. 1<2<3 looks fine, but Nix stops right at
the second <:
Each comparison takes exactly two sides. To check both, write two comparisons
and join them with &&, which means “and”:
You’ll mostly use comparisons to make decisions, like picking one setting when a
is low and another when it’s high.
Try it
Change 8080 to 80, so the comparison becomes true.
Sometimes one yes or no isn’t enough. You may want something only when two
switches are both on, or when at least one of them is. For that, Nix has &&,
|| and !.
&& is “and”, so both sides have to be true:
With ||, which is “or”, one true side is already enough:
And ! simply flips the boolean right after it:
Just like * goes before +, && goes before ||. So here, Nix works out
true&&false first, and only then the ||:
Reading it left to right would have given false. So when in doubt, simply
use .
Try it
&& goes first here, so this is true. Add parentheses around false || true to make the or go first.
Unlike most programming languages, you won’t find an if statement in Nix. What
it has instead is a similar mechanism, an
. It picks one of two : the one after then when the
condition is true, and the one after else when it’s false. Here’s how we
could use it to tell whether a needs root, since on Linux one below 1024
does by default:
Remember, the else is not optional, because if is an expression and has to
give back a value either way. Leave it out, and Nix won’t even read the code:
And since if gives back a value, the else can hold another if. That’s how
you pick from more than two values, like a battery that’s critical, low or
fine:
Nix checks the conditions in order, and stops at the first one that’s true.
It’s an expression like any other, so an if can sit right inside a
:
Try it
Change both 15s to 50, so neither condition is true and Nix falls through to the last else.
means “nothing here”. It isn’t 0, an empty or false. It’s
its own thing:
You’ll mostly see it in settings nobody has set yet. That’s handy whenever “not
picked yet” has to be different from every real answer. A setting that takes
true or false can start out as null, and Nix knows you simply haven’t
decided.
A in Nix is a bunch of
wrapped in square brackets. Unlike
most languages though, they are separated by spaces, not commas:
A list can hold any , from numbers and to even other lists:
You’ll see lists everywhere in Nix, from the installed on your system
to the files your pulls in.
Since spaces separate the items, something like 1+2 can’t just sit inside a
list:
Simply wrap it in , and Nix treats it as a single item:
This rule also applies to a whole lot of other expressions, like
,
and even a negative number. So -3 goes in as (-3):
So when an item is a calculation, a function call, an if or a negative number,
wrap it in .
Try it
Add (2 + 2) to the end of the list, so it counts up to 4.
Sometimes one list isn’t enough, and you want to glue two of them together.
That’s exactly what ++ is for:
The left comes first, and the right one goes on the
end. You’ll see this a lot when a list is built from a few parts, like the
on a machine. Here, their names stand in for them:
Both sides have to be lists, even when you only want to add one
item:
So simply wrap that one item in square brackets first, like [3].
Try it
The right side is a list holding another list, so [ 3 ] lands inside as one item. Remove the inner brackets, so it reads ++ [ 3 ].
An is one of the most important
kinds of in Nix. It holds names and values similar to dictionaries or
hashmaps in other languages and generally looks like
. Each name = value pair inside is called an
.
Every attribute ends with a semicolon, the last one included. And
when Nix prints a set, it sorts the names, so age comes before name here even
though we wrote it second.
A value can be anything Nix has, even another set. Here editor is a set of
its own, with its own names inside:
Both sets have a name, and they don’t clash. Each set keeps its own names.
You’ll meet attribute sets everywhere in Nix. Every flake.nix is one, and so
is every in and many NixOS . This set could go straight
into a NixOS . It names the machine and sets its time zone:
Sets inside sets can also be written with dots. This is the exact same set as
above, except it got a little more readable:
Names can also be written as . That comes in handy when a name has a dot
or a space in it:
Try it
Add editor = "neovim"; after age = 22;, inside the braces. Then look where Nix puts it in the result.
To get a out of an , we
simply put a dot and the name after it, just like getting a field from an object
in other programming languages:
And if a set holds another set, just keep adding dots:
A name written as a , like a file name with a dot in it, needs its quotes
here too. Without them, Nix reads files.hello.txt as two
steps, and looks for hello first:
Wrap the name in quotes, and Nix reads it as one name:
And no matter how you write it, the name has to match exactly. One letter off, and Nix stops:
Try it
Change the last line to read the GPU's vendor.
Selecting an that isn’t there is an error. This laptop has a name,
but nobody wrote down its cores:
So instead, put or and a fallback after the selection, and Nix simply uses the
fallback:
It even works on a longer path. This laptop has no gpu at all, and one or
still covers the whole thing:
You’ll see this all over , wherever a setting may be left out.
Note that or only covers a missing attribute. One that’s set to is
still there, so you get null:
Try it
Add cores = 8; after name = "laptop";, inside the braces, and the fallback is no longer needed.
Asking a set for a name it doesn’t have is an error. This machine has a
name, but no cores:
? lets you ask first. Write the set, a ? and a name, and you get a
instead of an error:
It follows dots too, so you can check deep inside a set:
You’ll see this check a lot in , whenever a setting can come in two
shapes, like a socket file or a network address.
Note that ? only checks that the name is there, not what it holds. Even a
name set to still counts.
Try it
Add cores = null; to the set, after name = "laptop";.
Sometimes you want a set that’s just like another one, with a small change.
That’s what // is for. It merges two
into one, and when a name is in
both, the right side wins:
But it only merges the top level. When both sides have a set under the
same name, the right one replaces the left one whole, and threads is gone:
In a real nixrepl, a set inside a set shows up as {...}. Put :p in front
to see all of it.
You’ll meet // all over . AppArmor, for example, is split into several
. Its PAM package takes the info of the main and swaps only
the description:
The homepage comes through untouched, so the other packages never have to
repeat it.
Try it
The merge drops threads. Bring it back: add threads = 8; to the cpu on the right side, after cores = 8;.
In a normal , the cannot use
each other’s names. version exists as an of the set, but not as a
name you can use inside it:
Put in front of the braces, short for recursive, and every value in the
set can use the other names in it. And just like in , the order doesn’t
matter:
You’ll see exactly this in files. Plenty of them in still
start with stdenv.mkDerivation rec {, so the version is written once and
reused further down.
Try it
Add url = "mirror://gnu/hello/" + name + ".tar.gz"; on a new line under the name line.
Sooner or later, the same starts showing up in several places. Here the
version of is typed twice:
Update one and forget the other, and the name no longer matches the download.
So instead, Nix gives us a very convenient
. Between let and
in, we can create as many as we want. It’s like
naming some variables, except these are immutable, so they never change. The
after in can then use them:
Change "2.12.3" to "2.12.2", an older release. Both the name and the address follow.
The version is written once now, and the result is the exact same as
before.
Every binding ends with a semicolon, just like in a set,
but the expression after in does not.
Bindings can even use each other, and their order doesn’t matter. Here name
is built from two bindings that only come after it:
And since the bindings only exist inside their own let, nothing leaks out
into the rest of your file. Here tag tries to use version from outside it:
Try it
Add file = name + ".tar.gz"; as the first binding. Then change the last line from name to file.
Joining with + gets noisy as soon as there are a few pieces:
So instead, Nix lets us drop a straight into a string with ${ }. This is
called , and the same address now
reads just like the result you want:
That’s the exact same string. ’s own in builds
its download link the same way.
Anything inside the braces is an , not
just a name:
But a number can’t go in there as it is:
That’s why you’ll so often see ${toStringport}. toString is a
built into Nix, and it turns the number into a string first:
scripts write their variables as ${HOME} too. To keep a literal ${
in a string, put a backslash in front of it:
In an , you put '' in front
of it instead. You’ll see this a lot in nixpkgs, because so much of the code in
there is shell scripts:
The prints both with a backslash, because that’s how a normal string
writes it. The text inside is just ${HOME}.
Try it
Add ${user} right after /home/, inside the quotes.
Add :${port} right after localhost, inside the quotes, and read the error. Then fix it by changing port to toString port.
Typing the same set name over and over gets old fast. Here every in the
starts with ports.:
So instead, lets you skip it. Put withports; in front, and after it ssh and
https mean ports.ssh and ports.https:
Same list, less typing. You’ll see it all the time in lists, where
withpkgs;[githtop] is just a shorter way to write
[pkgs.gitpkgs.htop].
with never hides a name you already have. Here defines
ssh too, so the list starts with 2222, not 22:
Meaning a name you defined yourself always wins. with only fills in the names
that aren’t defined yet.
Try it
Change [ ssh https ] to [ ssh http https ].
Make the list use the ssh from ports. Delete the line ssh = 2222;.
When you build a set out of you already named, you end up writing each
name twice:
So instead, writes those lines for you. List the names after it, and each one
becomes name=name;:
Same set, written shorter. So if you ever see inherit inputs; in a , it’s
just inputs=inputs;.
You’ll see it all over , very often passing a ’s pname and
version along to the code that downloads it.
Often the value you want sits inside another set. Put that set in ,
and inherit takes the names from there. These two give the same set:
inherit works in a too. Many NixOS pull the
helpers they need out of with a line like inherit(lib) mkOption types;.
Meaning the rest of the module can write mkOption instead of lib.mkOption.
Try it
Replace pname = pname; with inherit pname version;.
Change inherit (hello) pname; to inherit (hello) pname version;.
Nix is a functional programming language, so functions are at the core of
anything you do with it.
A is a little machine. Give it a
, and it gives you a new value.
In most languages, a function has a name, a list of parameters and a body. But
every Nix function is a , just like the ones you may know from other
languages. It has no name and exactly one .
Just write the parameter, a colon, and the body:
That lambda thing in the result is how Nix shows a function. The machine exists, but we haven’t
put anything into it yet.
To use it, put a value after it, separated by a space. The
keep the 5 from becoming part of
the body:
mynumber receives 5, so the body works out to 15.
But typing the whole function every time would get old quickly. A function is
a value like any other, so we can give our little machine a name with
, then use it as often as we like:
Add second = addTen 7; under first.
Now, about that one parameter. It may sound limiting, but a function can still
take as many inputs as you want. The trick is a function that gives back
another function. name:version: may look like two parameters, but what Nix sees is a function with a name
parameter, which returns another function with a version parameter. So we
simply pass the inputs one after another:
This trick is called , and you’ll see it all
over Nix code.
Try it
This double machine gives back exactly what it gets. Change its body from x to x * 2, so it lives up to its name.
name:version: looks like two , but it’s really a that gives
back another function. That means pkgName"hello""2.12.3" is just
(pkgName"hello")"2.12.3". The first call fills in name and hands back a
function, and the second one fills in version:
So you don’t have to pass every input at once. You can stop early, and
pkgName"hello" on its own is a perfectly good value. It’s a function still
waiting for its version:
This is called . hello is simply pkgName with the name
already filled in, which lets us reuse a part of our logic without writing a
new function.
You’ll see this shape all over Nix. An , for example, starts with
final:prev:, which is exactly this kind of function. So if you ever see a call
that seems to be missing an , this is why.
Try it
This git has the wrong name filled in. Change pkgName "hello" to pkgName "git".
A can take a whole as its
. Here pkgName takes a set called package, and reads pname and
version out of it:
This works, but the problem is that now we have to keep writing package.
throughout the body. Worse, anyone who wants to use this function has to read
all of it just to know which names the set needs.
So instead, Nix lets us unpack the set right in the . Write the names
you want in braces, like {pname,version}:, and they’re ready to use in the
body:
Same set in, same result out. But now the body is shorter, and anyone who sees
this function for the first time knows exactly how to use it.
Every listed name has to be there. Leave one out, and Nix stops with
function 'pkgName' called without required argument 'version'.
The order of the names in the call doesn’t matter, because Nix finds each
by its name. These two calls give the same result:
You’ll see set arguments a lot. Most NixOS start with one, and so do
files in , where the set names everything the package depends
on, like and fetchurl.
Try it
This call leaves out version, so Nix stops. Add version = "2.12.3"; to the set in the call, after pname = "hello";.
Sometimes a name in a set should be optional. Simply write ? and a
after it, like user?"root", and the function uses that value whenever
the call leaves user out:
A name without a default is still required. Leave out host, and Nix stops:
Package in use this all the time for optional features.
’s recipe, for example, has withVimKeys ? false. Meaning htop keeps its
normal keys, unless someone turns the Vim ones on.
Try it
Add user = "yurii"; to the set in the call, after host = "server";.
When every name has a default, an empty set is enough. Change host in the function to host ? "localhost", then change the call to sshTarget { }.
A with a set only accepts the names it lists. Pass it anything
extra, and Nix stops:
So instead, add at the end of the list, and the function simply takes any
extra names and ignores them:
You’ll see these dots all the time in . The configuration.nix the
graphical installer writes uses {config,pkgs,...}:, because NixOS calls it
with way more names than it needs.
In Nix, come in two shapes:
A plain single function:
default.nixNixA plain Nix expression.
And an unpacked set parameter function:
default.nixNixA plain Nix expression.
So what if you want both? That’s what the @-pattern is for. It unpacks
the so you can grab its
quickly, and also gives you the entire to
pass around:
default.nixNixA plain Nix expression.
In this example, {pname,...} and package represent the exact same thing.
package is the name of the function ,
and {pname,...} is also that exact same parameter, but destructured so we
can access its attributes.
You’ll see it often with , where outputs usually unpacks the to
grab nixpkgs, but also names the whole set inputs@ to pass it on, to
for example:
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
Interestingly enough, the order in which the parameter and its destructured
version come does not matter, so you could be writing param@{attr1} or
{attr1}@param, with any amount of spaces in between.
Try it
package holds the whole set. Change the body from pname to package.homepage.
Now use both at once. Change the body from pname to [ pname package.homepage ].
The Nix language itself is tiny. It has no keyword for counting a or
reading a file. Instead, those jobs are done by built right into Nix,
and they all live in one called
builtins.
You reach them just like any other . builtins.length counts the
items in a list:
A few of the most common ones, like toString, map and , are also
there under their short name. Most have no short name though, so
length on its own is not found:
Sets have their own builtins too. builtins.attrNames gives you all the names
in a set as a list:
And some builtins do things the language could never do on its own.
builtins.fromJSON turns text into a Nix set:
uses this all the time, like for reading a ’s download links out
of a sources.json file.
Nix comes with over a hundred builtins, so don’t try to learn the list. Just
look one up when you need it. In nixrepl, type builtins. and press Tab to
see them all, or search them on noogle.dev.
Try it
Swap builtins.attrNames for builtins.attrValues, which gives the values instead, in the same order.
Every in Nix has a . You never write the types
down, Nix simply works them out from the values themselves. builtins.typeOf
tells you which one you’ve got:
Even have a type, and it’s called "lambda". have one
too, "path".
Types still matter, because Nix checks them as it goes. Add a number to a
, and it stops:
When you need to check a value yourself, there are yes-or-no versions like
builtins.isString and builtins.isList.
Try it
Remove the quotes around "22", so the port becomes a number.
Nix has no loops. So to do the same thing to every item in a , we use map.
Give it a and a list, and you get back a new list with the function
applied to each item:
The function can be written right there, or have a name. These two give the
exact same list:
The function written in place needs
. They show Nix where the function ends
and the list begins.
You’ll meet map all over , whenever one setting has to be repeated for a
whole list of things, like users, devices or .
Try it
Put map toString in front of the list, so every port turns into a string.
Pull just the names out of these sets. Put map (user: user.name) in front of the list.
Nix is . It doesn’t work out a when you write
it down, only when something actually asks for it. An nothing uses is
never , so even a broken one, like 1/0, does no harm.
This is what makes usable at all. Its is one
with tens of thousands of names at the top level, and when you ask for hello, Nix works
out hello and what hello needs, not the other .
You can see it when nixeval or nixrepl prints a or a set. Each value
is worked out on its own, so one that fails shows up as «error: ...» in its
place, and everything around it still prints.
broken would fail, but the last line never asks for it.
Try it
The list asks for broken, so it prints an error in its place. Remove broken from the list, and leave its binding alone.
Sometimes code should refuse to go on. That’s what throw is for. Put it where
a would go, and when Nix reaches it, it stops right there with your
message:
Usually it sits in the branch of an that should never be taken. And like
everything in Nix, it only runs once it’s reached. 8080 passes the check, so
the throw is never touched:
Make it 70000, and it is:
uses this for common typos. Write pkgs.fetchFromGithub with a small
h, and stops with You meant fetchFromGitHub, with a capital H.
assert is a shorter way to write the same check. Instead of the bad case, you
write what has to be true, and you get the value after the ; when it is:
When it’s not, Nix stops:
With assert, you don’t get to write the message. Nix prints the condition the
way it reads it inside, which isn’t very friendly. Meaning assert is great for
checks that should never fail, and throw is better when a person will read the
message. You’ll see assert at the top of files in nixpkgs, checking
the before anything gets built.
A throw can even sit inside a set. Nothing happens until someone asks for
that one name:
You still got the , even though the set has a throw in it. That’s
how aliases.nix can be full of throws, and the rest of nixpkgs still works.
Try it
Ask for the certificate instead: change server.port to server.certificate. It hits the throw, so replace throw "no certificate yet" with "cert.pem".
lets refer to each other in any order. The one thing Nix cannot
do is work out a value that needs itself.
The plainest one is a value built from itself. To know x, Nix first needs
x+1, and for that it needs x again:
A circle can also go through two values. a needs b, and b needs a
again:
The classic way to get one is version=version; inside a set. It looks
like it copies version from the outside, but inside rec, the set’s own
names come first. So what Nix sees is a value defined as itself:
Nothing in this set actually needs rec. Drop it, and version comes from the
let outside again:
You’ll most likely meet this error in , when a decides which files
to import based on its own settings. To know its settings, NixOS has to read
every module, but to know which modules to read, it first needs the settings.
Conditional imports and infinite recursion
shows the fix step by step.
Try it
Keeping rec works too, if the outer value gets its own name. Rename the outer version in the let to v. Then change the line in the set to version = v;.
Files and directories are everywhere in Nix, so it has a separate kind of
just for them, called . A path always has a slash in
it, and goes without quotes:
default.nixProject filesA file in the example project.
A path starting with ./ or ../ is relative to the directory of the file it’s
written in. This file lives in /lesson, so that’s where ./ starts. Meaning
the path follows the file, not your .
Nix also works out .. right away, so modules/.. simply cancels out:
default.nixProject filesA file in the example project.
Adding a to a path gives you a longer path:
default.nixProject filesA file in the example project.
Note that a path cannot end with a slash. Write ./modules, not
./modules/, or Nix stops:
default.nixProject filesA file in the example project.
You’ll meet paths all over , like the
imports=[./hardware-configuration.nix]; near the top of your
configuration.nix.
Try it
This file lives in /lesson. Change ./ to ../, so the path starts one directory up.
default.nixProject filesA file in the example project.
This joins two strings. Remove the quotes around "/home/yurii" to make the left side a path, and the result turns into a path too.
One Nix file gets long fast. So you split it into several, and pulls
a file back in.
Every Nix file holds exactly one . import takes a
to a file and gives you that expression’s .
Here import./settings.nix gives back whatever settings.nix to:
settings.nixread only
default.nixProject filesA file in the example project.
Import a directory, and Nix uses the default.nix inside it. These two are the
same thing:
settings/default.nixread only
default.nixProject filesA file in the example project.
An imported file starts fresh. It can’t see the names of
the file that imports it. Here default.nix has an editor, but
settings.nix can’t use it:
settings.nixread only
default.nixProject filesA file in the example project.
That’s on purpose. A file means the same thing no matter who imports it.
So a file that needs something from outside is usually a
. It takes what it needs as an ,
and you call it right after importing it:
settings.nixread only
default.nixProject filesA file in the example project.
Don’t mix it up with imports, the you’ll see in configs. imports
lists for NixOS to merge in, while import just reads one file and
gives back its value.
is a directory too, and <nixpkgs> is the path to the nixpkgs on your
machine. So you can import it like any other file. What comes back is a function
that takes a set of settings, and {} means you want the defaults. Out comes
every :
You’ll see this line at the top of countless Nix files.
Try it
tools.nix is a function, and each machine calls it with its own editor. Add a third machine under the server: desktop = tools "emacs";.
tools.nixread only
default.nixProject filesA file in the example project.
All three machines share one file. Each call fills in its own editor.
Nix knows how to build software, but somebody still has to write down how to
build Firefox, or Neovim. Those all live in , one giant
on that the community maintains
together, and it’s massive. search.nixos.org
lists more than 140,000 from it.
And it’s not just packages. nixpkgs also holds , a of handy
, and every behind the NixOS . You don’t even need
to use it, because Nix on any Linux or macOS installs from the very same place.
NixOS / nixpkgs
›nixpkgs/
›pkgs/package recipes
›by-name/packages grouped by name
›ba/
›bat/
›package.nix
The recipe for building bat.
›bt/
›btop/
›package.nix
The recipe for building btop.
›fd/
›fd/
›package.nix
The recipe for building fd.
›gi/
›git/
›package.nix
The recipe for building Git.
›update.sh
A script for updating the Git package.
›he/
›hello/
›package.nix
The recipe for building GNU Hello.
›test.nix
A test that runs GNU Hello and checks its output.
›ht/
›htop/
›package.nix
The recipe for building htop.
›jq/
›jq/
›package.nix
The recipe for building jq.
›ju/
›just/
›package.nix
The recipe for building just.
›ne/
›neovim-unwrapped/
›package.nix
The recipe for building Neovim before adding its wrapper.
›treesitter-parsers.nix
Sources for the Tree-sitter parsers bundled with Neovim.
›ri/
›ripgrep/
›package.nix
The recipe for building ripgrep.
›tm/
›tmux/
›package.nix
The recipe for building tmux.
›wg/
›wget/
›package.nix
The recipe for building Wget.
›applications/packages grouped by category
›editors/
›vim/
›default.nix
The recipe for building Vim.
›macvim.nix
The recipe for building MacVim.
›networking/
›browsers/
›firefox/
›wrapper.nix
Adds the launcher and desktop integration around Firefox.
›update.nix
Creates a script for updating Firefox package versions.
›top-level/assembles the package set
›default.nix
Assembles the Nixpkgs package set.
›all-packages.nix
Connects package recipes to the attributes you type in pkgs.
›aliases.nix
Maps older package names to their replacements.
›lib/reusable Nix functions
›lists.nix
Functions for working with lists.
›strings.nix
Functions for working with strings.
›attrsets.nix
Functions for working with attribute sets.
›options.nix
Helpers for declaring and working with options.
›types.nix
Types used to check module option values.
›modules.nix
The machinery behind the module system.
›default.nix
Collects the library functions into lib.
›nixos/NixOS modules and tests
›modules/
›programs/
›git.nix
NixOS options and configuration for Git.
›firefox.nix
NixOS options and configuration for Firefox.
›fish.nix
NixOS options and configuration for the Fish shell.
›services/
›networking/
›ssh/
›sshd.nix
NixOS options and configuration for the OpenSSH server.
›web-servers/
›nginx/
›default.nix
NixOS options and configuration for nginx.
›vhost-options.nix
Options for individual websites served by nginx.
›config/
›users-groups.nix
NixOS options and configuration for users and groups.
›system-path.nix
Builds the system environment from the configured packages.
›system/
The modules that assemble the operating system.
›tests/
›firefox.nix
Tests Firefox on a virtual NixOS machine.
›nginx.nix
Tests nginx on virtual NixOS machines.
›openssh.nix
Tests OpenSSH on virtual NixOS machines.
›default.nix
The entry point for NixOS evaluation.
›maintainers/people caring for packages
›maintainer-list.nix
The people who look after packages.
›doc/project documentation
›README.md
How to contribute to the Nixpkgs reference manual.
›stdenv.md
Documentation for the standard package-building environment.
›functions.md
Documentation for Nixpkgs library functions.
›default.nix
The entry point that evaluates nixpkgs.
›flake.nix
The flake outputs the repository provides.
Open pkgs, then by-name, he and hello. That package.nix is the
recipe for , a tiny program that prints a greeting. Then open gi and
git next to it for Git’s recipe.
The two-letter directories like he and gi aren’t part of the
name. They’re just its first two letters. They sort all those packages into
smaller directories.
This is the top of hello’s package.nix, cut down to a few of its lines:
Don’t worry about reading every piece of it. Just notice that it
names the program, its version, and where to download the . That’s
the heart of every recipe.
When Nix nixpkgs, all those recipes turn into one giant
, where each package gets its
own name. So you never type the directory path. You select hello or git
from that set, the same way you select any other .
On the command line, nixpkgs# means “from nixpkgs”. So nixpkgs#hello.version asks nixpkgs
for hello, then for its version:
$ nix eval nixpkgs#hello.version"2.12.3"
The name is all you ever write, and you’ll write it everywhere. In a NixOS
, installing Git is the line
environment.systemPackages=[pkgs.git];. On the command line it’s
nixpkgs#git. Both mean the recipe from that git directory.
Wherever it comes from, that set usually goes by the name pkgs. In this ,
pkgs is already there for you, so go ahead and pick some packages out of it:
Change hello to git, and see which Git version this nixpkgs has.
pkgs.git, pkgs.firefox, pkgs.neovim. Almost every Nix file you’ll read
names like this. But isn’t built into Nix. It’s just a name
everybody agrees on for the giant from .
In a file of your own, you get it by importing nixpkgs and calling what comes
back. <nixpkgs> is the copy of nixpkgs on your machine:
Each package prints as a .drv file in the store. That’s its build ,
which Nix calls a . Meaning simply
selecting pkgs.git doesn’t build or install anything yet.
You can’t skip the {}. Without it, you only have the ,
and no packages at all:
That {} is where your settings go, and leaving it empty means you want the
defaults. For example, some packages, like Steam, aren’t open source. nixpkgs
calls them , and by default it refuses them:
Say yes with config.allowUnfree=true; in the set, and Steam is there too:
You won’t always import it yourself though. and hand pkgs
to your , and a gets it from its nixpkgs . But however
it arrives, pkgs.git is .
Try it
A package is a set too, and it knows a lot about itself. ripgrep's command isn't actually called ripgrep. Change description to mainProgram to find out what you really type.
Every page on search.nixos.org
shows a description, a license and a homepage. But where does all that come
from? From the package itself.
pkgs.hello is a , and like every
derivation, it’s just an . Most of it is build instructions for
Nix, but a few are there for you to read.
Start with the name. pname is the package’s name, version is its version,
and name simply joins the two with a dash:
meta holds everything you’d want to know before installing it, and that’s
exactly where the search page gets its text:
It also has mainProgram, the command the package gives you. That’s how
nixrunnixpkgs#ripgrep knows to start rg.
outputs lists the parts the package is built into, each with a of
its own. Hello is small, so it has a single output, out:
Note that reading these builds nothing. Meaning they’re written in
the , not files from the finished package, so you can look at any
package before you download it.
Try it
Hello comes in one part. Change pkgs.hello to pkgs.git, and see how many parts Git comes in.
comes in three parts: out with the program, doc with its documentation
and debug with its debugging symbols. Each part lands in the store on
its own, so you can have the program without the documentation.
aren’t the only thing in . is its of ordinary Nix
for lists, , and
version numbers, plus data like the almost 300 software licenses packages
can declare.
You’ll meet it as the lib at the top of package and of
and Home Manager , and as nixpkgs.lib in a . Hello’s recipe, for
example, writes license=lib.licenses.gpl3Plus; in its meta.
lib isn’t built into Nix. It’s just Nix code in the lib directory of
nixpkgs. Meaning it’s there wherever nixpkgs is, whether you’re on NixOS or not.
There are hundreds of functions in it, so instead of memorizing them, search
noogle.dev, which is like search.nixos.org, but for
functions.
Try it
Here are three of them at work. lib.licenses.gpl3Plus.fullName reads the
license’s full name, lib.versions.major picks the first number out of a
version, and lib.unique drops repeated items from a .
Change gpl3Plus to mit.
lib.versions has a neighbour that keeps two numbers. Change lib.versions.major to lib.versions.majorMinor.
This page needs the nix command, which is still experimental. Turn it on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Somebody has to call that function with all of those, and that’s
. You give it the to a recipe, it imports the file, looks at
the names the function asks for, takes each one from and makes the
call. Meaning a recipe never needs to know where nixpkgs comes from, it just
names what it needs.
Your own recipes work the same way. Here package.nix asks for , and
default.nix loads it with pkgs.callPackage./package.nix{}:
package.nixProject filesA file in the example project.
default.nixNIX_PATHThe Nix search path supplies `<nixpkgs>`.Project filesA file in the example project.
You've edited package.nix default.nix, but this output came from the original.
$ nix build --file default.nix$ ./result/bin/my-programhello from my-program
The set at the end wins over pkgs, so you can hand a recipe something else
without touching the file. nixpkgs does that itself: it defines fcitx5-lua
by calling its recipe with {lua=lua5_3;}, which gives it Lua 5.3 instead
of the default 5.2.
Every name the recipe asks for has to exist in pkgs or in that
set, unless the recipe gives it a default, because callPackage has nowhere
else to look. Ask for opensll by mistake, and it stops with
Functioncalledwithoutrequiredargument"opensll", followed by
did you mean "openslp", "openssl" or "openal"?.
Some in come with switches you can flip. Here’s the top of
’s recipe:
htop/package.nixexcerptNixA plain Nix expression.
The last is withVimKeys ? false. The ? false
means it’s off unless someone says otherwise. pkgs.htop is this recipe
called with the defaults. So how do you get the one with Vim keys?
That’s exactly what override is for. It calls the recipe again with the same
, except the ones you change. Everything you don’t mention stays as it was. It works
like // on the arguments: the old set, with your names on top.
nixpkgs itself defines htop-vim=htop.override{withVimKeys=true;};.
Here’s the same call in nixrepl:
Notice that the first list has two items and no package in it. The first
{...} is the override function. nixpkgs stores it in a set, so the
prints it like one.
You’ll see this pattern in plenty of configs, wherever someone wants a package
built a little differently than the default.
Say has a , but you want it a little different. Maybe built
from your local copy of the source, with an extra patch, or in a newer version
than nixpkgs ships. You don’t copy the for any of that. You change it
from the outside with overrideAttrs.
Most recipes in nixpkgs call with a set of : the
version, the src, the patches, the build flags and so on.
overrideAttrs makes a new package with some of them swapped.
Give it a set, and those attributes get replaced while the rest stay as they
were. Say you’re fixing a bug in itself, and want nixpkgs to build
your local copy instead of the release. Swap its src for the folder:
You get a new package, and hello itself stays the same. The new one is a
different recipe with its own , and the won’t have
it, so expect it to build on your machine.
Adding a patch is the most common reason to reach for overrideAttrs. A set is a bad fit for that, because it replaces the whole
. Pass a instead. It gets the old attributes, conventionally
named old, and returns the new ones. Here are both ways side by side:
replaced lost the recipe’s own patch. extended kept it and added yours to
the end.
Some attributes are built from others. Hello’s real recipe makes its download
link out of its own version, through finalAttrs. That’s what the attributes
look like after every change, yours included:
Inside the empty { }, add version = "2.12.2"; and watch the link follow.
The link moved, but the still describes the old , so a new
version needs a new hash as well. nixpkgs even prints a warning when you
forget.
One package in nixpkgs, scala-next, is nothing but that. It’s a newer release
of the regular Scala recipe with a new hash:
pkgs/by-name/sc/scala-next/package.nixProject filesA file in the example project.
inherit(oldAttrs.src) url; is short for url=oldAttrs.src.url;. The
regular Scala recipe builds that link from finalAttrs.version, so it already
points at 3.8.3.
There’s also override, which changes something else: the the
recipe’s function is called with.
Say you want with Vim keys. htop’s in has a switch for
that, withVimKeys, and pkgs.htop.override{withVimKeys=true;} builds
htop with it turned on. But that only gives you a new next to the old one.
Everything else that asks nixpkgs for htop by name still gets the plain one.
What you really want is to change what the name htop itself means. And that’s
exactly what an does. nixpkgs is one big of packages, and an overlay is
a small that hands back changes to it. Here’s one in its own file:
vim-htop.nixNixA plain Nix expression.
It takes two . prev is nixpkgs before your change. final is
nixpkgs with all the changes in, yours included. You’ll see in a moment why
you need both. The overlay returns a set with the names to add or replace,
here just htop.
In older code you’ll see them called self and super. Same thing, new
names.
You hand overlays to nixpkgs as a when you import it. Then pkgs.htop
itself is the Vim version:
For anything other than the name you’re replacing, take it from final. Then
you get every overlay’s changes. Here’s a second overlay that adds a monitor
command which starts htop:
monitor.nixNixA plain Nix expression.
Put it first in the list, before vim-htop.nix, and look at the script it
writes:
monitor.nix reads from a set that isn’t finished yet. Meaning
this only works because Nix is . It looks inside
final only once something asks for a , and by then every overlay is
in.
How far it carries
overrideone build, through its inputscall the package's own function again with different arguments
overrideAttrsone build, through its recipechange the attributes mkDerivation was handed, after that function ran
anoverlayeveryone who asks for that namechange what the name itself means inside the package set
An you hand to import<nixpkgs> only changes that one pkgs. But
your NixOS system gets its pkgs from NixOS itself, so how do you get your
overlay in there?
Simple: overlays go in the nixpkgs.overlays option of your
configuration.nix:
configuration.nixNixOS moduleA NixOS module.Project filesA file in the example project.
The second overlay is written right in the . You don’t need a file for
every overlay.
Now programs.htop.enable installs htop-vim. That asks for
pkgs.htop, and the name now means your version. has the same
nixpkgs.overlays .
share overlays too. rust-overlay is a flake that adds Rust toolchains
to . Its overlay is the overlays.default :
Meaning a flake can hand you new names in , like rust-bin here.
In a NixOS flake, rust-overlay is one more . Its overlays.default goes
in nixpkgs.overlays like the overlays above, and then any can install
from pkgs.rust-bin.
Most spread a program across your system. Here’s where
, a tiny program that prints a greeting, ends up on Debian:
The command goes into , its manual into /usr/share/man, and the
it needs sit in /usr/lib, all mixed in with every other program’s
files.
Nix puts everything it installs into one directory, /nix/store, and gives each
a directory of its own there, on Linux and macOS alike. Here’s Hello
again, this time in the store. Open it and look around:
Inside, the program sits in bin, and its manuals and translations in
share. The directories around it are the libraries Hello runs on.
Once something lands in the store, Nix never changes it again. Every file in
there is read-only, and a new version goes into a new directory, right next to
the old one.
That’s exactly what lets dozens of different versions of the same software live
side by side without conflicts. Programs that worked yesterday still work today,
and a faulty update is just an extra broken version you don’t have to use. Here
are four Neovim versions in one store. Say the newest one broke. The other three
still work exactly like before:
So how does your find nvim? Nothing gets copied anywhere. In a
, your shell gets pointed at one of those directories:
$ which nvim/nix/store/7602dj25chzbdnipsh66lzqm6qjssllk-neovim-0.12.4/bin/nvim
Which version that is depends on the nixpkgs your devshell comes from. On
, your shell looks in /run/current-system/sw/bin instead. That’s a
directory of links, and each one points into the store.
Here’s where lives on a Linux machine:
/nix/store/9lmc8ad19psaj2cpwav6a0yzjqlrn00j-hello-2.12.3. It has three parts:
/nix/store/ is the store itself.
9lmc8ad19psaj2cpwav6a0yzjqlrn00j is a , always 32 characters.
hello-2.12.3 is the name, for a usually its name and version.
Every path in the store is named this way.
The name is there for you. Nix tells paths apart by the hash, which is why
different builds of the very same version can sit next to each other. Here’s
one machine that has four of them:
$ ls -d /nix/store/*-hello-2.12.3/nix/store/5z2yp3ysx8476c8g5w25b0smlgkjvaq3-hello-2.12.3/nix/store/9lmc8ad19psaj2cpwav6a0yzjqlrn00j-hello-2.12.3/nix/store/lcxc2z1h2g8wq8dxfpizy1di85kyxnqx-hello-2.12.3/nix/store/xl1h9i29pgq2q5cszjhm5wpfxfbbqwyi-hello-2.12.3
Same program, same version, four hashes. Each of these was built against a
different build of , the C , so to Nix they are four different
packages, and none of them gets in the way of the others.
That’s because the hash isn’t random. For a package, Nix works it out from the
whole : the , the build script, the system it’s built for, and
every , each of which already carries a hash of its own. Change any of
that, even one flag, and the hash changes with it.
So the same recipe lands at the same path on every machine, and Nix knows where
a package will end up before building anything. Here we ask one version
of where GNU Hello goes on Linux and on an Apple Silicon Mac, and
nothing gets built to answer:
The system is part of the recipe, so we get two hashes. Run the first command
on any Linux machine or Mac and you get exactly this path, which is what lets
Nix download a package built somewhere else instead of building it again.
Files Nix copies straight into the store, like your own source code, are the
exception. Their hash comes from the contents and the name. nixstoreadd
copies a file into the store and prints its path, so let’s add the same note
from two directories, then change it:
$ cat notes.txtbuy milk$ nix store add notes.txt/nix/store/gfgd4jkdhmdv4m7rlvdj1785i84kh74i-notes.txt$ nix store add backup/notes.txt/nix/store/gfgd4jkdhmdv4m7rlvdj1785i84kh74i-notes.txt$ echo "buy bread" > notes.txt$ nix store add notes.txt/nix/store/b1wlbkjd4cc57pp738v50a34bjdckg6c-notes.txt
The same contents land at one path, wherever they came from. New contents get
a new hash.
These paths aren’t directories. A can be a single file,
and gfgd4jkdhmdv4m7rlvdj1785i84kh74i-notes.txt is just the note itself. The
build recipes ending in .drv are single files too.
And one package can be split into several paths. , a tool for reading ,
puts its command, its library, its headers and its manuals in separate paths:
The jq command sits in the one ending in -bin, and the library in the one
with no ending. So a program that only needs the library doesn’t pull in the
command or the manuals.
Every in the store was built from a . In Nix, that recipe is
called a , and it’s just an that says what to run, and on which system.
The simplest way to make one is the
derivation . Give it a set, and it gives you a derivation back:
name ends up in the .
system says which kind of machine can build it, here a 64-bit Intel or AMD
Linux.
builder is the program Nix runs, here a .
args is what the gets, so the shell runs
echohelloworld>$out as a script.
$out isn’t Nix. It’s a shell variable, and Nix sets it to the one
place in the store the builder is allowed to write to. Whatever the builder
leaves there becomes the package, here a single file that says helloworld.
this didn’t build anything. Instead, Nix wrote the recipe into the
store as a file ending in .drv, and that’s the path the printed. This
step is called .
The .drv file is the whole recipe, written down without a single line of Nix
code:
$ cat /nix/store/dp5na63mzhc3a2kkiy37b3xq642k7lhy-hello-world.drvDerive([("out","/nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world","","")],[],[],"x86_64-linux","/bin/sh",["-c","echo hello world > $out"],[("builder","/bin/sh"),("name","hello-world"),("out","/nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world"),("system","x86_64-linux")])
It already knows the exact path the result will land at, before anything is
built. And every input in there is an exact store path too. Meaning you could
hand this file to another machine, and it could build it without ever seeing
your Nix code.
Running the recipe is called . nix-build takes a file, writes the
recipe, and builds it, all in one go:
hello-world.nixNixA plain Nix expression.
You've edited hello-world.nix, but this output came from the original.
$ nix-build hello-world.nixthis derivation will be built: /nix/store/dp5na63mzhc3a2kkiy37b3xq642k7lhy-hello-world.drvbuilding '/nix/store/dp5na63mzhc3a2kkiy37b3xq642k7lhy-hello-world.drv'.../nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world$ cat resulthello world$ nix-build hello-world.nix/nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world
The last line is the new store path, and result is a
to it. The second run builds nothing at all,
because that exact path is already in the store. As long as it stays there,
this recipe never needs building again.
The builder does have to create $out itself though. Remove > $out from the
script, and the build fails, because nothing was left behind:
$ nix-build hello-world.nixthis derivation will be built: /nix/store/m8s2qvxd5rag0kr3i2agyyq7ckjidri0-hello-world.drvbuilding '/nix/store/m8s2qvxd5rag0kr3i2agyyq7ckjidri0-hello-world.drv'...hello worlderror: builder for '/nix/store/m8s2qvxd5rag0kr3i2agyyq7ckjidri0-hello-world.drv' failed to produce output path for output 'out' at "/nix/store/m8s2qvxd5rag0kr3i2agyyq7ckjidri0-hello-world.drv.chroot/root/nix/store/wmhy6sa98m5s8gf4m2r5nmkp1v6sa8ky-hello-world"
Every package in is a derivation too, and they print the exact same way:
Their recipes use , which fills in a much bigger set for
you. But in the end, it calls this same derivation function.
Try it
Change the last line to hello-world.outPath. That's where the result will land, before anything is built.
The script is part of the recipe. Change hello world in the script to hello nix. The name stays the same, but the hash doesn't, because this is a different package now.
This page needs the nix command, which is still experimental. Turn it on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
The derivation builds anything, as long as you hand it everything
yourself: a program, a system and every tool along the way. Here’s a
small C program built that way:
default.nixNIX_PATHThe Nix search path supplies `<nixpkgs>`.Project filesA file in the example project.
Note how even mkdir and gcc have to be handed over by hand. Now imagine
doing that for over a hundred thousand , writing the same build script
over and over again.
So instead, sets those tools up once. is the standard
environment: a C , make, , coreutils, tar, patch and a few
more, all ready to go.
And builds with them. It takes a set that describes your
package and turns it into a , so you
only write down what’s special about your package.
Almost every in nixpkgs is a call to it, or to something built on top of
it. Here’s the real recipe for , cut down to a few of its lines:
pname and version name the package. Together they make its full name:
src says where the comes from, here a that fetchurl
downloads.
And there isn’t a single build command in there! Hello is an ordinary GNU
project, so mkDerivation simply does what you’d do by hand: unpack the source,
then run ./configure, make and makeinstall. doCheck=true; adds
makecheck to that.
You could hand mkDerivation a plain set instead, but then the version is
typed twice:
hello/package.nixNixA plain Nix expression.
This works, but the problem is that you can bump version, forget the URL, and
build the old source under the new name. That’s why Hello passes a function
instead. The function gets the package’s own finished as
finalAttrs, so the URL can simply use finalAttrs.version. Meaning a new
release only needs a new version and hash, and the URL follows.
Your own program works the same way. Put a default.nix next to its source,
and src=./.; hands mkDerivation the directory it’s in:
default.nixNIX_PATHThe Nix search path supplies `<nixpkgs>`.Project filesA file in the example project.
If your project builds the GNU way, like Hello, that’s already the whole
recipe. This directory holds a small C program with a configure script of its
own, and that file is all it takes:
$ lsconfigure default.nix main.c Makefile.in$ nix build -L --file default.nixmy-program> Running phase: unpackPhasemy-program> unpacking source archive /nix/store/jgysx33grs0k68w8irhkkiy45x74198j-my-programmy-program> source root is my-programmy-program> Running phase: patchPhasemy-program> Running phase: updateAutotoolsGnuConfigScriptsPhasemy-program> Running phase: configurePhasemy-program> patching script interpreter paths in ./configuremy-program> ./configure: interpreter directive changed from "#!/bin/sh" to "/nix/store/7n1hai8jk6vgc967drgkqi6k7j6ff28q-bash-5.3p9/bin/sh"my-program> configure flags: --prefix=/nix/store/afx97f427sgm5hsmwxdnj7va4iv0rlpz-my-program-1.0my-program> Running phase: buildPhasemy-program> build flags: SHELL=/nix/store/7n1hai8jk6vgc967drgkqi6k7j6ff28q-bash-5.3p9/bin/bashmy-program> gcc -o my-program main.cmy-program> Running phase: installPhasemy-program> install flags: SHELL=/nix/store/7n1hai8jk6vgc967drgkqi6k7j6ff28q-bash-5.3p9/bin/bash installmy-program> mkdir -p /nix/store/afx97f427sgm5hsmwxdnj7va4iv0rlpz-my-program-1.0/binmy-program> cp my-program /nix/store/afx97f427sgm5hsmwxdnj7va4iv0rlpz-my-program-1.0/binmy-program> Running phase: fixupPhasemy-program> shrinking RPATHs of ELF executables and libraries in /nix/store/afx97f427sgm5hsmwxdnj7va4iv0rlpz-my-program-1.0my-program> shrinking /nix/store/afx97f427sgm5hsmwxdnj7va4iv0rlpz-my-program-1.0/bin/my-programmy-program> checking for references to /build/ in /nix/store/afx97f427sgm5hsmwxdnj7va4iv0rlpz-my-program-1.0...my-program> patching script interpreter paths in /nix/store/afx97f427sgm5hsmwxdnj7va4iv0rlpz-my-program-1.0my-program> stripping (with command strip and flags -S -p) in /nix/store/afx97f427sgm5hsmwxdnj7va4iv0rlpz-my-program-1.0/bin$ ./result/bin/my-programhello from my-program
That’s the exact same gcc, mkdir-p and cp you wrote by hand at the top,
except this time you didn’t write any of it. stdenv brought the tools, and the
project’s own configure and Makefile did the rest.
That’s your first package. From here, pick the language your project is written
in, and follow its page:
C,
Python or
Rust.
Try it
The description has the version typed in by hand. Replace 1.0 in it with ${finalAttrs.version}. Then change version to "2.0", and the description follows.
This page needs the nix command, which is still experimental. Turn it on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Most C projects start their with a long of things to install first,
and then the command that builds it. With Nix, a right next to your code
replaces that whole list. Anyone with Nix runs one command, and your program
builds, on any machine.
Here’s a small C program that prints which it was built with. Its
finds OpenSSL with and installs into PREFIX:
The recipe lives in package.nix, next to the code. It’s a that asks
for everything the build needs. src=./.; copies the project directory into
the store, and the build runs from there:
package.nixProject filesA file in the example project.
pkg-config runs during the build, so it goes in nativeBuildInputs.
openssl is the the program links against, so it goes in
buildInputs. And makeFlags hands make the ’s as its
PREFIX, so makeinstall puts the program right there.
default.nix loads the recipe with , which hands it ,
pkg-config and openssl from :
default.nixNIX_PATHThe Nix search path supplies `<nixpkgs>`.Project filesA file in the example project.
You never wrote a single build step. The default run make
and makeinstall for you, and -L lets you watch them in the log:
$ nix build --file default.nix -Lmy-project> Running phase: unpackPhasemy-project> unpacking source archive /nix/store/1mqsa5xfxgqy06h96r5fqymrhm7a99yn-my-projectmy-project> source root is my-projectmy-project> Running phase: patchPhasemy-project> Running phase: updateAutotoolsGnuConfigScriptsPhasemy-project> Running phase: configurePhasemy-project> no configure script, doing nothingmy-project> Running phase: buildPhasemy-project> build flags: SHELL=/nix/store/7n1hai8jk6vgc967drgkqi6k7j6ff28q-bash-5.3p9/bin/bash PREFIX=\$\(out\)my-project> gcc -o my-project main.c -I/nix/store/i2ahcmvfd1pzv0p4lz822x12lav0rnam-openssl-3.6.4-dev/include -L/nix/store/7fr737xfi9qw3fzvdsqmnbqid56knndp-openssl-3.6.4/lib -lssl -lcryptomy-project> Running phase: installPhasemy-project> install flags: SHELL=/nix/store/7n1hai8jk6vgc967drgkqi6k7j6ff28q-bash-5.3p9/bin/bash PREFIX=\$\(out\) installmy-project> mkdir -p /nix/store/4b7mznf1hix1r605wpl4w217fkcbm88n-my-project-0.1.0/binmy-project> cp my-project /nix/store/4b7mznf1hix1r605wpl4w217fkcbm88n-my-project-0.1.0/binmy-project> Running phase: fixupPhasemy-project> shrinking RPATHs of ELF executables and libraries in /nix/store/4b7mznf1hix1r605wpl4w217fkcbm88n-my-project-0.1.0my-project> shrinking /nix/store/4b7mznf1hix1r605wpl4w217fkcbm88n-my-project-0.1.0/bin/my-projectmy-project> checking for references to /build/ in /nix/store/4b7mznf1hix1r605wpl4w217fkcbm88n-my-project-0.1.0...my-project> patching script interpreter paths in /nix/store/4b7mznf1hix1r605wpl4w217fkcbm88n-my-project-0.1.0my-project> stripping (with command strip and flags -S -p) in /nix/store/4b7mznf1hix1r605wpl4w217fkcbm88n-my-project-0.1.0/bin$ ./result/bin/my-projectmy-project, built with OpenSSL 3.6.4 25 Aug 2026
Look at the gcc line. pkg-config filled in the OpenSSL flags for it.
And the build only sees what you list. Leave out nativeBuildInputs, and
pkg-config is missing, even if it’s installed right there on your machine:
$ nix build --file default.nixerror: Cannot build '/nix/store/g0jlkwykba4dwhi3gk8phmaxxa472jfr-my-project-0.1.0.drv'. Reason: builder failed with exit code 2. Output paths: /nix/store/36hw77d3hafabrki3icibh8kbcw6srq2-my-project-0.1.0 Last 15 log lines: > Running phase: unpackPhase > unpacking source archive /nix/store/xa8c92g4gg6nfgi3xm6178p3j6y9a5j5-my-project > source root is my-project > Running phase: patchPhase > Running phase: updateAutotoolsGnuConfigScriptsPhase > Running phase: configurePhase > no configure script, doing nothing > Running phase: buildPhase > build flags: SHELL=/nix/store/7n1hai8jk6vgc967drgkqi6k7j6ff28q-bash-5.3p9/bin/bash PREFIX=\$\(out\) > /nix/store/7n1hai8jk6vgc967drgkqi6k7j6ff28q-bash-5.3p9/bin/bash: line 1: pkg-config: command not found > gcc -o my-project main.c > /nix/store/bikkd0jwr8jkd3r6r5mcr28fm17yvb0z-binutils-2.46/bin/ld.bfd: /build/ccMkHjf3.o: in function `main': > main.c:(.text.startup+0x7): undefined reference to `OpenSSL_version' > collect2: error: ld returned 1 exit status > make: *** [Makefile:4: my-project] Error 1 For full logs, run: nix log /nix/store/g0jlkwykba4dwhi3gk8phmaxxa472jfr-my-project-0.1.0.drv
Look for pkg-config:commandnotfound in the log. Without it, the Makefile
gets no flags for OpenSSL. So gcc runs without the library, and the build
stops.
That’s it, your project is a Nix package now. Commit package.nix and
default.nix next to your code, and anyone can build it with one command.
This page needs the nix command, which is still experimental. Turn it on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Got a Python project? Then you’ll want python3Packages.buildPythonApplication.
It knows how Python projects are built, so you don’t write a single build step.
And the Python your program needs are already in ,
under python3Packages, so the simply lists them.
Here’s hello-py, a tiny Python program that prints some ASCII art with the
art library. Its pyproject.toml says what it needs, like any modern Python
project:
The builder also checks your against pyproject.toml. Forget
art in dependencies, and the build stops before you ever ship a program
that crashes on its first import. The log is cut down to … here:
$ nix build --file default.nixerror: Cannot build '/nix/store/cycmwycsvxcbgvqbd9ficggrl05brhyz-hello-py-0.1.0.drv'. Reason: builder failed with exit code 1. … > Running phase: pythonRuntimeDepsCheckHook > Executing pythonRuntimeDepsCheck > Checking runtime dependencies for hello_py-0.1.0-py2.py3-none-any.whl > - art not installed
Recipes in nixpkgs look exactly the same. Here’s termdown, a countdown timer
for your :
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Rust projects come with , and Cargo wants to download your
right in the middle of the build. But a Nix build isn’t allowed on the network.
So watch what happens with hello-rs, a tiny Rust program that uses one ,
colored, built with a plain that simply runs cargobuild:
default.nixNIX_PATHThe Nix search path supplies `<nixpkgs>`.Project filesA file in the example project.
-v makes nixbuild say what it builds:
You've edited default.nix, but this output came from the original.
$ nix build -v --file default.nixthis derivation will be built: /nix/store/mmgh2v92rmw24y574l2dqpryz969yv7b-hello-rs-0.1.0.drvbuilding '/nix/store/mmgh2v92rmw24y574l2dqpryz969yv7b-hello-rs-0.1.0.drv'...error: Cannot build '/nix/store/mmgh2v92rmw24y574l2dqpryz969yv7b-hello-rs-0.1.0.drv'. Reason: builder failed with exit code 101. Output paths: /nix/store/22lzkhrvmf087ydnv4y1m7jn21vliaq4-hello-rs-0.1.0 Last 22 log lines: > Running phase: unpackPhase > unpacking source archive /nix/store/mb5qj081d4682aq15lvj9aszkj410lqy-hello-rs > source root is hello-rs > Running phase: patchPhase > Running phase: updateAutotoolsGnuConfigScriptsPhase > Running phase: configurePhase > no configure script, doing nothing > Running phase: buildPhase > Updating crates.io index > warning: spurious network error (3 tries remaining): [6] Could not resolve hostname (Could not resolve host: index.crates.io) > warning: spurious network error (2 tries remaining): [6] Could not resolve hostname (Could not resolve host: index.crates.io) > warning: spurious network error (1 try remaining): [6] Could not resolve hostname (Could not resolve host: index.crates.io) > error: failed to get `colored` as a dependency of package `hello-rs v0.1.0 (/build/hello-rs)` > > Caused by: > download of config.json failed > > Caused by: > failed to download from `https://index.crates.io/config.json` > > Caused by: > [6] Could not resolve hostname (Could not resolve host: index.crates.io) For full logs, run: nix log /nix/store/mmgh2v92rmw24y574l2dqpryz969yv7b-hello-rs-0.1.0.drv
Cargo goes to crates.io for colored, and inside the build there’s no network to
reach it.
So instead, has rustPlatform.buildRustPackage. It downloads your
dependencies in a separate step first, and that step may use the network,
because Nix checks what it gets against a , cargoHash. Then it runs
cargobuild for you.
With buildRustPackage, there’s no buildPhase or installPhase to write at
all. But you can’t know the dependency hash before the first build. So cargoHash
starts out as lib.fakeHash, a placeholder made of As. The build fails, and
tells you the real one:
default.nixNIX_PATHThe Nix search path supplies `<nixpkgs>`.Project filesA file in the example project.
You've edited default.nix, but this output came from the original.
The -vendor-staging is that separate download step, and it’s the
one that failed. The real hash is right there, after got:.
Put "sha256-0BwTOd0u9Y2aV2jog0WKiAfMhTatiNX8yahqj4FFIE8=" in place of
pkgs.lib.fakeHash, and the build goes through:
$ nix build -v --file default.nixthese 2 derivations will be built: /nix/store/9kgi6ma7zlypfcdg679w0zvxyvwx6pi6-hello-rs-0.1.0-vendor.drv /nix/store/g4zh2nixzycjdmj2vq19hcxs23sixx74-hello-rs-0.1.0.drvbuilding '/nix/store/9kgi6ma7zlypfcdg679w0zvxyvwx6pi6-hello-rs-0.1.0-vendor.drv'...building '/nix/store/g4zh2nixzycjdmj2vq19hcxs23sixx74-hello-rs-0.1.0.drv'...$ ./result/bin/hello-rshello world
cargoHash describes the dependencies locked in Cargo.lock, not
your own code. So you can change your code all you want, and keep the same hash. Change Cargo.lock,
say to add a dependency, and the next build stops with
ERROR:cargoHashorcargoSha256isoutofdate until you put in the new one.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Put your in a , and anyone in the world can
build or run it with one command, without even cloning your .
nixbuild and nixrun need to know where your package is. In a flake, that
place is the packages . First comes the system the package is built
for, like x86_64-linux for an ordinary 64-bit Linux PC. Then comes the
package’s name. Together they make packages.x86_64-linux.my-project.
Below is a small C project’s in a flake. It builds a program called
my-project that prints which it was built with:
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
It needs openssl, plus to find it. makeFlags tells its
to install into $out, the package’s directory in the store.
nixflakeshow lists everything a flake offers:
You've edited flake.nix, but this output came from the original.
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Here’s something cool. This runs nh, a helper for rebuilding NixOS, straight
from its on , without installing anything. Nix builds it first,
so the first run takes a few minutes:
$ nix run github:nix-community/nh/v4.4.2 -- --versionnh 4.4.2
Notice that nobody typed a name! That works because nh’s flake has
a package called default, and nixrun starts that one whenever you don’t
name another. Plain nixbuild builds that one too.
Your own default usually isn’t a second though. It simply points at the
package you already wrote. self is your flake’s own , so
self.packages.x86_64-linux.my-project is the my-project package below. Meaning default
and my-project will build exactly the same package.
Try it
Below the my-project package, add packages.x86_64-linux.default = self.packages.x86_64-linux.my-project;.
Offer packages.x86_64-linux.default
Point it at my-project
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
Now nixflakeshow lists both names, and plain nixrun starts your program:
You've edited flake.nix, but this output came from the original.
$ nix flake showgit+file:///tmp/my-project?ref=refs/heads/master&rev=f7b9e593932b871540e45ed7b0853621a3b14e93└───packages └───x86_64-linux ├───default: package 'my-project-0.1.0' └───my-project: package 'my-project-0.1.0'$ nix runmy-project, built with OpenSSL 3.6.4 25 Aug 2026
Push your flake, and anyone can run your program the exact same way as nh.
Turn the small Node.js project from the devshell simulation into a anyone
can build and run.
Takes about 25 minutes.
You’ll:
write its with buildNpmPackage
let the first build fail, and paste in the hash it tells you
run your program on its own, outside the devshell
move the recipe into default.nix and start it with nixrun
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Building a program from source usually takes a few steps you’d run by hand:
unpack the source, ./configure, make, makeinstall. The that
gives you is simply a script that runs those same steps for
you, one after another. Each step is called a .
These are the main ones, in the order they run:
unpackPhase unpacks src.
patchPhase applies your patches.
configurePhase runs ./configure, if the project has one.
buildPhase runs make.
checkPhase runs the project’s tests.
installPhase runs makeinstall.
fixupPhase tidies up the result, for example by cutting debug info out of
programs to make them smaller.
Tests are off by default. checkPhase only runs when you set doCheck=true;,
so a plain build skips them.
But not every project has a configure script or a . When a default
doesn’t fit, you simply set that phase to your own code. Take a project
that’s just one C file:
There’s nothing for make to run, so the says how to compile it and
what to keep:
default.nixNIX_PATHThe Nix search path supplies `<nixpkgs>`.Project filesA file in the example project.
$out is the ’s path in the store. installPhase has to copy the
program there, because the build directory is thrown away afterwards and only
$out is kept.
With -L, nixbuild shows the phases as they run:
$ nix build -L --file default.nixmy-program> Running phase: unpackPhasemy-program> unpacking source archive /nix/store/g9dc05gsb6fh89dkfpqa24xx2h1xv2k2-my-programmy-program> source root is my-programmy-program> Running phase: patchPhasemy-program> Running phase: updateAutotoolsGnuConfigScriptsPhasemy-program> Running phase: configurePhasemy-program> no configure script, doing nothingmy-program> Running phase: buildPhasemy-program> Running phase: installPhasemy-program> Running phase: fixupPhasemy-program> shrinking RPATHs of ELF executables and libraries in /nix/store/93wm9sc4clw7majwgj25xvcqli7c1gmx-my-program-1.0my-program> shrinking /nix/store/93wm9sc4clw7majwgj25xvcqli7c1gmx-my-program-1.0/bin/my-programmy-program> checking for references to /build/ in /nix/store/93wm9sc4clw7majwgj25xvcqli7c1gmx-my-program-1.0...my-program> patching script interpreter paths in /nix/store/93wm9sc4clw7majwgj25xvcqli7c1gmx-my-program-1.0my-program> stripping (with command strip and flags -S -p) in /nix/store/93wm9sc4clw7majwgj25xvcqli7c1gmx-my-program-1.0/bin$ ./result/bin/my-programhello from my-program
checkPhase is missing from the log because doCheck isn’t set.
updateAutotoolsGnuConfigScriptsPhase is an extra step slips in
before configurePhase.
Leave installPhase out, and the build fails. The default one finds no Makefile
and does nothing, so nothing ends up in $out:
$ nix build -L --file default.nix…my-program> Running phase: installPhasemy-program> no Makefile or custom installPhase, doing nothingmy-program> Running phase: fixupPhaseerror: builder for '/nix/store/6llxpzmirj8fzmi8pgcnwim0imkm9qzi-my-program-1.0.drv' failed to produce output path for output 'out' at "/nix/store/6llxpzmirj8fzmi8pgcnwim0imkm9qzi-my-program-1.0.drv.chroot/root/nix/store/ag1jb0i6qfn1vvj3808hjbbmclw0ddwq-my-program-1.0"
This page needs the nix command, which is still experimental. Turn it on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
A program rarely builds from its source alone. It needs tools that do the
building, like a or , and it links against, like or
. A keeps those in two separate : nativeBuildInputs for the
tools, and buildInputs for the libraries. Open almost any recipe in ,
and you’ll find both.
Here’s the recipe for a small Qt app built with CMake. Its CMakeLists.txt
asks for Qt with find_package(Qt6REQUIREDCOMPONENTSWidgets). Its
package.nix fills both lists:
package.nixProject filesA file in the example project.
cmake and qt6.wrapQtAppsHook run while the builds, so they go in
nativeBuildInputs. qt6.qtbase is the Qt library the program links
against, so it goes in buildInputs.
A default.nix next to the recipe loads it. fills in the
recipe’s , like cmake and qt6, from nixpkgs. The build log
starts like this, with the long line of cmake flags cut down to …:
You've edited package.nix, but this output came from the original.
$ cat default.nixlet pkgs = import <nixpkgs> { };inpkgs.callPackage ./package.nix { }$ nix build -L --file default.nixmy-app> Running phase: qtPreHookmy-app> Running phase: unpackPhasemy-app> unpacking source archive /nix/store/v8h46r0dhvjk1lfrx86ylcgvj3pvjibd-my-appmy-app> source root is my-appmy-app> Running phase: patchPhasemy-app> Running phase: updateAutotoolsGnuConfigScriptsPhasemy-app> Running phase: configurePhasemy-app> cmake flags: -DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=OFF …my-app> -- The CXX compiler identification is GNU 15.3.0my-app> -- Detecting CXX compiler ABI info
Notice the very first , qtPreHook. A plain build has no such phase.
It comes from qtbase. Any package in either list can bring a , a
bit of code that adds phases or changes what they do.
Then look at configurePhase. On its own, that step runs a ./configure
script. In this build it runs cmake instead, and the lines after it are
CMake’s own. The cmake hook swapped the step out. The same hook points
find_package at everything in buildInputs, so find_package(Qt6...)
finds qtbase without a single path in the recipe.
wrapQtAppsHook is the one Qt apps cannot skip. Qt loads its plugins while the
program runs. wrapQtAppsHook handles that by putting each installed program
inside a small that tells Qt where the plugins are. The same recipe
without it looks like this:
package.nixProject filesA file in the example project.
Leave the hook out, and the build stops right away:
You've edited package.nix, but this output came from the original.
$ nix build --file default.nixerror: Cannot build '/nix/store/x0wabb04b46nsqm9zbl10izdgfj81dcb-my-app-1.0.drv'. Reason: builder failed with exit code 1. Output paths: /nix/store/haw1db6q8qcimfibra3nvbvkq73kwx6i-my-app-1.0 Last 4 log lines: > Running phase: qtPreHook > Error: this derivation depends on qtbase, but no wrapping behavior was specified. > - If this is an application, add wrapQtAppsHook to nativeBuildInputs > - If this is a library or you need custom wrapping logic, set dontWrapQtApps = true For full logs, run: nix log /nix/store/x0wabb04b46nsqm9zbl10izdgfj81dcb-my-app-1.0.drv
The qtPreHook from before is what stops it. qtbase checks for the
wrapper while the package builds. So you never ship a Qt app that can’t find
its plugins.
So the rule of thumb is simple. If it runs while the package builds, it goes in
nativeBuildInputs. If your program uses it once it’s built, it goes in
buildInputs.
This page needs the nix command, which is still experimental. Turn it on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Often a default is almost right, and you only want one more step. For
that, the main phases each have two : one that runs at the start of the phase,
and one at the end.
preBuild runs at the start of buildPhase, and postBuild at its end.
preInstall and postInstall do the same inside installPhase.
And so on, for the other main phases. A hook adds a step without replacing the phase.
ddgr’s in never writes a phase at all. ddgr is a tool
that searches DuckDuckGo from your . Its installs the
program and its manual, and a single hook adds what it leaves out, the shell
completions:
ddgr/package.nixexcerptNixA plain Nix expression.
$(out) is how make reads the out variable, the package’s store path. So
makeinstall puts ddgr into $out. After that, postInstall adds
completions for Bash, fish and zsh.
The installShellCompletion command comes from installShellFiles. That’s why
installShellFiles is in nativeBuildInputs.
Here’s my-program, which writes its own installPhase, with a postInstall
added next to it:
default.nixNIX_PATHThe Nix search path supplies `<nixpkgs>`.Project filesA file in the example project.
The build works, but done never shows up in the log:
A phase you write yourself skips its hooks! That’s because the default
installPhase is what calls them, and your own code replaces it whole, calls
included.
The fix is to call the hooks yourself with runHook, at the start and end of
your phase:
default.nixNIX_PATHThe Nix search path supplies `<nixpkgs>`.Project filesA file in the example project.
Now done shows up in the log, right inside the install phase:
That’s why most recipes in nixpkgs that write their own installPhase start
and end it with these two lines. Here’s sl, the steam locomotive that rolls
across your when you mistype ls:
sl/package.nixexcerptNixA plain Nix expression.
A build normally runs without network access, so it cannot download
its source halfway through. are the exception. fetchurl,
fetchFromGitHub, fetchgit and friends from are that are
allowed online, because you write down the of what they’ll download before
they run.
If the download matches the hash, Nix keeps it in the store, and anyone who
builds with that hash gets exactly the same files. If it doesn’t, the build
stops with a hash mismatch. Here’s how gets its source:
mirror://gnu is simply shorthand for the GNU download mirrors.
The hash has to change whenever the version does, because a new
version is a different file. But how do you get the hash of a file you haven’t
even downloaded yet? You simply let Nix tell you:
Put in lib.fakeHash, a placeholder made of As.
Build. The download doesn’t match, so the build fails.
Copy the hash from the error’s got: line into your .
Say you want the older Hello 2.12.2. Its file is different, so it needs a new
hash. Here’s the whole loop in nixrepl, where :b builds whatever you give
it:
$ nix replNix 2.34.8Type :? for help.nix-repl> pkgs = import <nixpkgs> { }nix-repl> :b pkgs.fetchurl { url = "mirror://gnu/hello/hello-2.12.2.tar.gz"; hash = pkgs.lib.fakeHash; }error: hash mismatch in fixed-output derivation '/nix/store/10hp2cdxxhr9vspqw0ispvcr13mspsyi-hello-2.12.2.tar.gz.drv': specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= got: sha256-WpqZbcKSzCTc9BHO6H6S9qrluNE72caBm0x6nc4IGKs=[0 built (1 failed), 0.0 KiB DL]nix-repl> :b pkgs.fetchurl { url = "mirror://gnu/hello/hello-2.12.2.tar.gz"; hash = "sha256-WpqZbcKSzCTc9BHO6H6S9qrluNE72caBm0x6nc4IGKs="; }This derivation produced the following outputs: out -> /nix/store/dw402azxjrgrzrk6j0p66wkqrab5mwgw-hello-2.12.2.tar.gz
The same trick works for every fetcher, and for every other hash Nix will ever
ask you for.
This page needs the nix command, which is still experimental. Turn it on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Here’s a small C project, packaged with a right next to its code. A
default.nix beside it loads the recipe with :
package.nixProject filesA file in the example project.
src=./.; is handy, but it takes the whole directory. That means any change in
it makes a new source, and a new build. Even fixing a typo in README.md does.
-v makes nixbuild say what it builds:
$ echo "Fixed a typo." >> README.md$ nix build -v --file default.nixthis derivation will be built: /nix/store/cbkw6ahhncvhfhkr74izavh0ilp1xvl3-my-project-0.1.0.drvbuilding '/nix/store/cbkw6ahhncvhfhkr74izavh0ilp1xvl3-my-project-0.1.0.drv'...
So instead, pick the files yourself. lib.fileset.toSource puts only the files you
list into the store. The recipe now asks for too:
package.nixProject filesA file in the example project.
root is the directory the files come from. unions joins your into one set of files.
Now only main.c and the Makefile go into the store. Build it once, then
fix another typo:
$ nix build -v --file default.nixthis derivation will be built: /nix/store/m6414cdljhx0g5vcx91vdcjd6p5z5qi1-my-project-0.1.0.drvbuilding '/nix/store/m6414cdljhx0g5vcx91vdcjd6p5z5qi1-my-project-0.1.0.drv'...$ echo "Fixed another typo." >> README.md$ nix build -v --file default.nix
The second build printed nothing. The never made it into
the store, so there was nothing new to build.
This page needs the nix command, which is still experimental. Turn it on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
A that reads src from the directory next to it only builds where the
code is. But once you publish a release, src can download it from
instead. Then the recipe can live anywhere, in your own config, or even in
.
Here’s how that looks for a real project. hcxtools is a C project on GitHub, and
its finds with . This recipe asks for
fetchFromGitHub, and uses it in src. Put it in a directory as package.nix,
next to a default.nix that loads it with :
package.nixProject filesA file in the example project.
You don’t know the yet, so leave it empty. Just like lib.fakeHash, the
build stops and tells you the right one. -v makes nixbuild say what it builds:
Copy the hash after got: into hash, and build again:
$ nix build -v --file default.nixthis derivation will be built: /nix/store/2raz6q02bpplv858s1n6xxbfb08b3hsb-hcxtools-7.1.2.drvbuilding '/nix/store/2raz6q02bpplv858s1n6xxbfb08b3hsb-hcxtools-7.1.2.drv'...$ ./result/bin/hcxpcapngtool --versionhcxpcapngtool 7.1.2 (C) 2026 ZeroBeat
Now compare it with the hcxtools recipe that really lives in nixpkgs:
hcxtools/package.nixNixA plain Nix expression.
It’s the recipe you just wrote, with the exact same hash! Only a few small things
differ:
meta describes the , its license and who looks after it.
finalAttrs.version reuses the version, so it’s written only once.
placeholder"out" is another way to write the package’s store path.
sha256 is an older name for hash.
Meaning your package.nix could go into nixpkgs almost as it is. And that’s
exactly how most packages get there: somebody needed a program, wrote a recipe,
and sent it in.
A build that could read anything on your computer would quietly depend on
whatever happens to be installed there, and break on the next machine. So on
Linux, Nix runs every inside a , a small isolated world made for
that one build. The builder sees the its lists, a
temporary build directory, and a few basics like /dev and /proc. Your , the programs installed on your system and the network all stay
outside.
Nix also clears the , so the builder doesn’t get your either. Let’s ask a builder where its home is:
builder-home.nixNixA plain Nix expression.
You've edited builder-home.nix, but this output came from the original.
$ nix-build builder-home.nixthis derivation will be built: /nix/store/zixy215m44cq116qr0xmczqfmbhv5qn8-builder-home.drvbuilding '/nix/store/zixy215m44cq116qr0xmczqfmbhv5qn8-builder-home.drv'.../nix/store/2sd4i6fhq8q2rgsd8y1bscgzr3wbsf4n-builder-home$ cat result/homeless-shelter
Nix sets HOME to /homeless-shelter, a directory that doesn’t exist, on every
machine. Inside the sandbox there’s no /home either, so even a builder that
goes looking for your files by their full path finds nothing. So a build
cannot pick up a that happens to be on your laptop, or download
something different tomorrow, because it cannot reach either.
The one exception is a download with its written in the . It gets
network access, because Nix checks the result against that hash anyway.
On macOS, Nix leaves the sandbox off by default, so there it’s up to the
recipe to behave.
Sooner or later one needs the exact path of another, for a script, a
config file or a keyboard shortcut. Put a inside a with
${ }, and Nix puts its there.
Every derivation has an outPath . It’s the store path where its
result will land. Put the string and outPath in one and compare:
They’re the same path. Meaning ${ } just reads outPath. A
derivation is only an , so any set with an outPath works, even
one you write by hand:
You’ll see this all over , because its start programs this way.
Here is how the nullmailer in runs its program:
${pkgs.nullmailer} turns into the store path of the nullmailer package, and
/bin/nullmailer-send goes after it. Meaning the service always runs the
exact nullmailer your built, by its full path.
But the string also quietly remembers which derivation it came from. Use it
inside another derivation, and Nix makes that derivation an input of the
build, without you listing it anywhere. This second derivation reads the file
hello-world built and copies its text:
$ nix-store --query --references /nix/store/kjwi369mnwfm0xblisf5afxnhzh73jig-hello-world-copy.drv/nix/store/dp5na63mzhc3a2kkiy37b3xq642k7lhy-hello-world.drv$ nix-build /nix/store/kjwi369mnwfm0xblisf5afxnhzh73jig-hello-world-copy.drvthis derivation will be built: /nix/store/kjwi369mnwfm0xblisf5afxnhzh73jig-hello-world-copy.drvbuilding '/nix/store/kjwi369mnwfm0xblisf5afxnhzh73jig-hello-world-copy.drv'.../nix/store/7ffhsri1q0qjy6qd2ymy48zs5rra3isg-hello-world-copy$ cat resulthello world
The first command lists what the depends on. There’s the .drv of
hello-world, even though the script only mentions a path. On a fresh
machine, Nix would build hello-world first. This machine had built it
already, so Nix only built the copy.
The only gets to read the inputs of its recipe. Now paste the same
path in as plain text, without ${ }:
$ nix-store --query --references /nix/store/yij30cpw5gffkzaicqd3s4ql9xp1yyr6-hello-world-copy.drv$ nix-build /nix/store/yij30cpw5gffkzaicqd3s4ql9xp1yyr6-hello-world-copy.drvthis derivation will be built: /nix/store/yij30cpw5gffkzaicqd3s4ql9xp1yyr6-hello-world-copy.drvbuilding '/nix/store/yij30cpw5gffkzaicqd3s4ql9xp1yyr6-hello-world-copy.drv'...sh: can't open /nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world: no such file/nix/store/chyvbdy3gz9vy2hkqa2kq33ayqfhdpql-hello-world-copy$ cat result
The script still reads the exact same path. But the recipe has no
inputs now, so the builder can’t see that file, even though it’s right there
on this machine. The build still finishes, because the script carries on after
read fails. So the copy comes out empty. Only ${ } brings hello-world
along.
Try it
Change the text hello-world writes from hello world to hello nix. The copy's path changes too, because its script holds the path of hello-world.
This page needs the nix command, which is still experimental. Turn it on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Copy only hello’s directory to another machine, and it won’t even start. ’s program calls into , the C . It finds glibc through a written right into its files, and that path isn’t there.
The store paths hello points to are its . --references lists them:
Hello mentions its own path too, so it shows up in its own references.
Nobody wrote this by hand. A Debian carries a list of
the packages it depends on, and apt trusts that list. Nix doesn’t need one.
After every build, it scans the output for store , and every hash it
finds becomes a reference.
References lead to more references. The of a path is the path itself
plus everything it refers to, all the way down. --requisites lists the whole
closure:
The closure is the unit Nix moves around. nixcopy sends the whole closure to
another machine. Downloading hello from cache.nixos.org makes sure all five paths are
there. The keeps the whole closure of everything you still
use.
Meaning the closure is the size you really pay, and it can be more than you’d
expect:
$ du -shc $(nix-store --query --requisites /nix/store/9lmc8ad19psaj2cpwav6a0yzjqlrn00j-hello-2.12.3)2.0M /nix/store/39m23yla3lnw563f4a1sa6hnn0dgjyj1-libunistring-1.4.2412K /nix/store/8pf3c5kh87gpzyfm8cc3lym5j14npzyw-libidn2-2.3.8200K /nix/store/hna4zsz4rdnl4mdbmwmks7cx3cdkkyqc-xgcc-15.3.0-libgcc35M /nix/store/znb3q6g1ik34454j3vcjx824h1871asg-glibc-2.42-84384K /nix/store/9lmc8ad19psaj2cpwav6a0yzjqlrn00j-hello-2.12.338M total
Hello itself takes 384K on this disk, and almost all of its 38M closure is
glibc. The good news is that programs from the same share it. Here’s
coreutils pointing at the very same glibc path:
All 3294 get built, copied and kept together, as one unit.
Removing a doesn’t delete anything from the store. Uninstalling it,
switching to a without it, or leaving a temporary only
stops pointing at its path, and the path stays right where it was. That makes
going back to an older setup cheap, and it’s also why the store keeps growing.
Cleaning up is the ’s job. It starts from
, the paths Nix knows are in
use: your , which hold what you installed
along with every earlier version of it, the result links nix-build leaves
behind, and whatever running programs are using right now. Everything in the
of a root stays. Everything else is garbage.
You can see a root at work with the nix-store--delete command. It deletes
one path, but only if that path is garbage. Right after building
a tiny called hello-world, its result link is still in the directory:
$ nix-store --delete /nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-worldfinding garbage collector roots...0 store paths deleted, 0.0 KiB freederror: Cannot delete path '/nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world' since it is still alive. To find out why, use: nix-store --query --roots and nix-store --query --referrers
Nix refuses, because the path is “still alive”: the result link is a root
pointing at it. Remove the link, and the same command deletes the path.
To clean up everything at once, run nix-collect-garbage. Add --dry-run
first, and it only counts what would go. Here it runs on a store where the only
garbage is hello-world and the .drv file it was built from:
$ nix-collect-garbage --dry-runfinding garbage collector roots...determining live/dead paths...2 store paths would be deleted$ nix-collect-garbagefinding garbage collector roots...deleting garbage...deleting '/nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world'deleting '/nix/store/dp5na63mzhc3a2kkiy37b3xq642k7lhy-hello-world.drv'deleting unused links...note: hard linking is currently saving 0.0 KiB2 store paths deleted, 0.3 KiB freed
nix-collect-garbage-d goes further. It first deletes the old
of your profiles, the earlier versions
you could go back to. So whatever only they used becomes garbage too.
Here a profile has four generations. Firefox was installed in generation 2 and
removed in generation 3:
$ nix-env --list-generations 1 2026-10-13 19:55:10 2 2026-10-13 19:57:42 3 2026-10-13 20:00:12 4 2026-10-13 20:02:43 (current)$ nix-collect-garbage -dremoving old generations of profile /home/alice/.local/state/nix/profiles/profileremoving profile version 1removing profile version 2removing profile version 3finding garbage collector roots...…deleting garbage...…deleting '/nix/store/1i8glb8gw744gwpsj5qwpya9qlck4ing-firefox-157.0'…deleting unused links...note: hard linking is currently saving 0.0 KiB3364 store paths deleted, 1.3 GiB freed
The old generations were still holding 1.3 GiB, mostly Firefox
and the it needs. That’s the price of being able to roll back. Once the old generations are gone, so is the way back to them.
-d is all or nothing. --delete-older-than30d is the gentler version. It
keeps the generations from the last 30 days, plus the one you were on 30 days
ago, and deletes the rest.
On , run these with to clean up old system generations.
With sudo, they only touch root’s profiles and the system’s, not yours. So run
them once more without sudo to clean up your own.
NixOS can also run this for you. Set nix.gc.automatic=true; in
configuration.nix, and the collector runs every night at 03:15. Want it
weekly? Set nix.gc.dates="weekly";. Want it to drop old generations too?
Set nix.gc.options="--delete-older-than 30d";.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Installing a from usually means
downloading it, not compiling it, even though every package there is a build
. That works because the in a comes from the recipe.
So Nix knows the path before it builds anything.
Before building anything, Nix asks a whether it already has
that exact path. If it does, Nix downloads it as a
and skips the build. The default cache
is cache.nixos.org, where the build farm uploads what it builds from
nixpkgs.
Here’s being installed on a machine that doesn’t have it yet.
-v makes nixbuild say what it fetches and builds:
$ nix build -v github:NixOS/nixpkgs/774debe7a0d1b496e35677ad955a1011c6ff74f3#hellothese 5 paths will be fetched (10.8 MiB download, 36.2 MiB unpacked): /nix/store/znb3q6g1ik34454j3vcjx824h1871asg-glibc-2.42-84 /nix/store/9lmc8ad19psaj2cpwav6a0yzjqlrn00j-hello-2.12.3 /nix/store/8pf3c5kh87gpzyfm8cc3lym5j14npzyw-libidn2-2.3.8 /nix/store/39m23yla3lnw563f4a1sa6hnn0dgjyj1-libunistring-1.4.2 /nix/store/hna4zsz4rdnl4mdbmwmks7cx3cdkkyqc-xgcc-15.3.0-libgcccopying path '/nix/store/hna4zsz4rdnl4mdbmwmks7cx3cdkkyqc-xgcc-15.3.0-libgcc' from 'https://cache.nixos.org'...copying path '/nix/store/39m23yla3lnw563f4a1sa6hnn0dgjyj1-libunistring-1.4.2' from 'https://cache.nixos.org'...copying path '/nix/store/8pf3c5kh87gpzyfm8cc3lym5j14npzyw-libidn2-2.3.8' from 'https://cache.nixos.org'...copying path '/nix/store/znb3q6g1ik34454j3vcjx824h1871asg-glibc-2.42-84' from 'https://cache.nixos.org'...copying path '/nix/store/9lmc8ad19psaj2cpwav6a0yzjqlrn00j-hello-2.12.3' from 'https://cache.nixos.org'...
Nothing got compiled. Every line is a download from
cache.nixos.org. When you see copyingpath...from, the cache had it.
A binary cache is basically just a remote . Asking it about a path is
a plain web request. The file is named after the hash part of the path, here
the 9lmc8ad... of hello:
URL is where the packed up hello sits on the cache. References lists what
hello needs at run time. Besides hello itself, that’s just .
The Sig line at the end is a signature. Nix only accepts a download that a
key it trusts has signed. Otherwise, anyone who answers that web request could
hand you a different hello under the same path. The key of cache.nixos.org is
trusted out of the box.
Now take a tiny nobody else has built, and look at its path:
Nobody built that recipe for the cache, so Nix builds it on your machine.
The same happens when you change a package. Change one flag, and the
hash changes. Meaning there’s nothing to download.
Your own projects can get the same. A server builds every and
pushes the results to a cache, just like the build farm does for nixpkgs.
Here’s a model of that. Flip what goes into your build, and see if
the cache still has your path:
The cache CI pushed to has this path. Nothing is compiled.
cache.nixos.org only has what the build farm builds from nixpkgs. That’s why
many bigger projects run a cache of their own, and their tells you how
to add it. nix-community, the organisation behind , keeps one
at https://nix-community.cachix.org.
To use a cache, Nix needs two things: its URL in substituters, and its
public key in trusted-public-keys, so it can check the signatures. On NixOS,
both are in your :
It has to be that file. By default, only trusted users like root may add
caches. Put the same lines in your own ~/.config/nix/nix.conf, and Nix skips
them with a warning:
$ nix build github:NixOS/nixpkgs/774debe7a0d1b496e35677ad955a1011c6ff74f3#hellowarning: ignoring untrusted substituter 'https://nix-community.cachix.org', you are not a trusted user.Run `man nix.conf` for more information on the `substituters` configuration option.warning: ignoring the client-specified setting 'trusted-public-keys', because it is a restricted setting and you are not a trusted user
Try it
Change echo hello world to echo hello nix, and watch the hash in the path change.
This page needs the nix command, which is still experimental. Turn it on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Not every needs a source and a build. Sometimes all you want
is a small script on your , and has
writers for exactly that: in that take a name and some text
and turn them into a package.
pkgs.writeShellScriptBin"name""text" puts your text in bin/name under a
Bash shebang. pkgs.writeShellApplication goes further, and takes a set: the
name, the script as text, and runtimeInputs, the packages whose commands
the script needs. The text is usually an , so it starts at the
left edge no matter how deep your Nix code is.
The PATH line lets work inside the script even if it isn’t installed
anywhere else. set -o errexit stops the script at the first failing command.
$USER reached the script untouched, because only ${ } means
something to Nix inside the . Everything else is left for .
writeShellApplication also checks the script with ShellCheck while building.
Drop the quotes, as cowsay hello, $USER, and the build fails before the
script ever runs:
$ nix build -v --file default.nixthis derivation will be built: /nix/store/jprby5llv61yx5b95b2snk30nj9x9isr-greet.drvbuilding '/nix/store/jprby5llv61yx5b95b2snk30nj9x9isr-greet.drv'...error: Cannot build '/nix/store/jprby5llv61yx5b95b2snk30nj9x9isr-greet.drv'. Reason: builder failed with exit code 1. Output paths: /nix/store/46yv09g955l115zasijyg2c382y24iq0-greet Last 10 log lines: > > In /nix/store/46yv09g955l115zasijyg2c382y24iq0-greet/bin/greet line 8: > cowsay hello, $USER > ^---^ SC2086 (info): Double quote to prevent globbing and word splitting. > > Did you mean: > cowsay hello, "$USER" > > For more information: > https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ... For full logs, run: nix log /nix/store/jprby5llv61yx5b95b2snk30nj9x9isr-greet.drv
Without the quotes, Bash would split the value of $USER at spaces and expand
any * in it. ShellCheck catches slips like that before anyone runs the
script.
Sometimes a program needs a little push before it starts: an extra flag, or an
telling it where its files are. A is exactly that.
It’s a tiny script that sits where the program used to be, sets things up, and
then starts the real program.
does this all the time. gifgen is a small shell script that turns a
video into a GIF with ffmpeg. Without ffmpeg on your , it stops with
“Error: gifgen requires ffmpeg to be installed”. So its wraps it:
lib.makeBinPath[ffmpeg] is ffmpeg’s bin directory in the store. So
gifgen finds ffmpeg even on a machine where nobody installed it.
makeWrapper goes in nativeBuildInputs, because it only runs during the
build. It gives the build a wrapProgram command. That’s the whole trick, and
close to 2,000 files in nixpkgs do it.
--prefix is one of several things a wrapper can do:
--set NAME VALUE sets an environment variable.
--prefix NAME : VALUE adds to the front of a like . That’s how gifgen finds ffmpeg.
--suffix NAME : VALUE adds to the end.
--add-flags puts extra flags in front of yours.
This page needs the nix command, which is still experimental. Turn it on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Wrapping isn’t only for the people who write a . You can wrap any
that’s already built, without it at all.
The trick is symlinkJoin. It makes a new package out of
to the packages in paths, and postBuild
runs right after. That’s where the wrapping goes.
Here’s a recipe that gives a default flag, --smart-case. With it, a
lowercase pattern matches any case:
package.nixProject filesA file in the example project.
Build it, and look at what wrapProgram left in bin:
$ nix build --file default.nix$ cat result/bin/rg#! /nix/store/7n1hai8jk6vgc967drgkqi6k7j6ff28q-bash-5.3p9/bin/bash -eexec -a "$0" "/nix/store/sp83if3jqdyqbxl2ar9rgqr84gdiw8ig-ripgrep/bin/.rg-wrapped" --smart-case "$@" $ echo "Hello from Nix" > notes.txt$ ./result/bin/rg hello notes.txt1:Hello from Nix
wrapProgram renamed the original rg to .rg-wrapped and wrote this script
in its place. It runs the real ripgrep with --smart-case first and your own
arguments after it, in "$@". Ripgrep itself came from the untouched. The only thing built here is the directory of symlinks and the
two-line script.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Most programs read their settings from a in your , so a
alone never carries your setup. But many programs also let an
point at their whole config file. Put that variable in a
, and your travels with the package, on every machine,
with nothing to put in ~/.config.
is one of them. It reads its flags from the file named in
RIPGREP_CONFIG_PATH, one flag per line. So keep a ripgreprc next to the
:
$ cat ripgreprc--smart-case--hidden
Then point the wrapper at it:
package.nixProject filesA file in the example project.
--hidden is new here. It makes ripgrep search hidden files too, the ones
whose name starts with a dot. Build it, and the wrapper now sets the variable
before it starts ripgrep:
$ nix build --file default.nix$ cat result/bin/rg#! /nix/store/7n1hai8jk6vgc967drgkqi6k7j6ff28q-bash-5.3p9/bin/bash -eexport RIPGREP_CONFIG_PATH='/nix/store/rq0ld36zvij6l12yw72a363j21308x6d-ripgreprc'exec -a "$0" "/nix/store/xlpcxc7qyn1wd34wrxdq06x261bzllpm-ripgrep/bin/.rg-wrapped" "$@" $ rm notes.txt$ echo "Hello from Nix" > .notes.txt$ ./result/bin/rg hello.notes.txt1:Hello from Nix$ nix run nixpkgs#ripgrep -- hello
ripgreprc got its own place in the store. Nix simply copied it there along
with the package, so the wrapper never depends on the directory you built
it in. Plain ripgrep finds nothing, because the file is hidden and its Hello
has a capital H.
Put (pkgs.callPackage./package.nix{}) in environment.systemPackages or
home.packages in place of pkgs.ripgrep, and every machine searches the same
way.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
A wrapped carries its own , so it works the same on any
machine without . Here’s with Ctrl+A as its prefix key and Vim
keys in copy mode, wrapped by hand:
package.nixProject filesA file in the example project.
symlinkJoin makes a new package that links to tmux’s files. wrapProgram
then swaps its bin/tmux for a small script that starts the real tmux with
-f and your config.
It works, but look at how much you had to know. You wrote the config in tmux’s
own format. You had to know that tmux takes its config file with -f. And the
next program wants it all told differently. Alacritty wants , and takes
its file with --config-file.
nix-wrapper-modules does that part for you. It’s a that builds wrapped
packages with the . You set , the same way you would in
or , and it writes the config file and the . It also
comes with ready-made wrapper for about 60 programs, like tmux,
WezTerm, Alacritty and Neovim. BirdeeHub maintains it under the
nix-community
organisation on .
Here’s the same tmux again. A wrapper module looks just like a NixOS module:
tmux.nixProject filesA file in the example project.
wlib is the library’s own set of and modules, and every wrapper
module gets it. This one imports the ready-made tmux module from it, and sets
three of its options.
To turn the module into a package, add the library to your ’s , and
hand the module to wrappers.lib.evalPackage:
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
evalPackage takes a of modules. The first one is a tiny module that
only sets , so the wrapper knows where to take tmux from. Run nixbuild, and look at what it
made:
You've edited flake.nix tmux.nix, but this output came from the original.
The module turned your three options into lines of a tmux.conf in the same
package, and the wrapper starts the real tmux with -f pointing at that file.
Every option you didn’t set keeps the default the module picked, so the file
has a lot more in it, like set -g mouse on.
The result is a regular package, with its config baked in. Run it
with nixrun, put it in a , or install it on a machine that has
never seen your dotfiles. It’s the same tmux everywhere.
You can also install it straight from your NixOS configuration. The library’s
flake has a NixOS module for every wrapper. Add wrappers.nixosModules.tmux to
your system’s modules, and the same options appear under wrappers.tmux:
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
terminal.nixNixOS moduleA NixOS module.Project filesA file in the example project.
enable=true; puts the wrapped tmux in your system packages. Home Manager
works the same way, with wrappers.homeModules.tmux.
Try it
The tmux module turns mouse support on by default. Turn it off. In terminal.nix, add mouse = false; inside wrappers.tmux.
Turn the mouse off
terminal.nixNixOS moduleA NixOS module.Project filesA file in the example project.
On , Nix is already installed. On macOS or any other Linux,
the installer from nixos.org/download sets it
up next to your existing system. This is the command most people want:
That’s the multi-user install. Pick it on macOS, and on any Linux that runs
, has SELinux turned off and lets you use sudo.
It creates /nix and puts the inside it. A background
, the Nix , runs every build as a separate build user, one
that can’t write to your home directory. So a build never touches your own
files.
If your machine doesn’t fit, use the single-user install. It skips the daemon,
so builds run as you, and /nix belongs to you:
When it finishes, open a new terminal. The installer added a few lines to your
’s startup files to put nix on your , and only a new terminal
reads them. Check that nix is there. Your version will likely be newer than
this one:
$ nix --versionnix (Nix) 2.34.8
Flakes and nix commands are still experimental. Turn them on first:
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Now try one. nixrun downloads and runs it, without installing
anything:
$ nix run nixpkgs#helloHello, world!
If you ever want Nix gone again, the Nix manual has
uninstall steps for
each kind of install.
Nix comes with two sets of commands, and guides use both, so it helps to tell
them apart at a glance.
The legacy commands have a dash in their name: nix-build, nix-shell,
nix-env, nix-store, nix-instantiate, nix-channel and
nix-collect-garbage. They’ve been around for many years, they’re stable, and
they work on every install right away. You point them at a .nix file, and
they find through <nixpkgs>, the copy your setup points to.
The experimental commands are one nix command with a subcommand after a
space: nixbuild, nixshell, nixrun, nixdevelop, nixflake and
more. They were designed together with , so most of them take a flake
address like nixpkgs#hello. Their interface can still change, and a fresh
install leaves them switched off.
Here’s the same job, evaluating 6*7, done both ways:
The dash matters. nixbuild and nix-build look for different
files. Try both in a directory with only a flake in it:
$ lsflake.lock flake.nix$ nix build$ lsflake.lock flake.nix result$ nix-builderror: path '/home/alice/project/default.nix' does not exist
The trickiest pair is nix-shell and nixshell. With -p, both give you
hello. Without it, nix-shell goes looking for a file instead:
$ nix-shell -p hello --run helloHello, world!$ nix shell nixpkgs#hello --command helloHello, world!$ nix-shellerror: no argument specified and no 'shell.nix' or 'default.nix' file found in the working directory
Older guides and the manual mostly use the dashed ones. Flake use
the new ones. Since the dash changes what runs, type the command exactly as the
guide writes it.
Both sets work on the same . Here’s hello built with each from the
same nixpkgs, and both land on the very same :
$ nix-build '<nixpkgs>' -A hello/nix/store/9lmc8ad19psaj2cpwav6a0yzjqlrn00j-hello-2.12.3$ nix build nixpkgs#hello --print-out-paths/nix/store/9lmc8ad19psaj2cpwav6a0yzjqlrn00j-hello-2.12.3
Meaning a built by one is already there for the other. So you can mix
them freely. Many people use the experimental commands for their flakes and
still reach for a legacy one now and then.
Plenty of and guides still tell you to run nix-build, or
nix-build'<nixpkgs>'-Ahello. It builds a Nix file and leaves a result
link to what it built in the current directory. The newer command for the same
job is nixbuild.
Here’s a tiny that writes one line into its output:
hello-world.nixNixA plain Nix expression.
Give nix-build the file, and it builds it:
You've edited hello-world.nix, but this output came from the original.
$ nix-build hello-world.nixthis derivation will be built: /nix/store/dp5na63mzhc3a2kkiy37b3xq642k7lhy-hello-world.drvbuilding '/nix/store/dp5na63mzhc3a2kkiy37b3xq642k7lhy-hello-world.drv'.../nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world$ readlink result/nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world$ cat resulthello world
The last line nix-build prints is the it built, and result
points right at it.
Leave the file out, and nix-build builds default.nix from the current
directory:
default.nixProject filesA file in the example project.
You've edited default.nix, but this output came from the original.
Nothing was built this time. hello-world was already in the store from the
last run.
A file can also hold a whole set of . Here packages.nix has two:
packages.nixNixA plain Nix expression.
-A picks one of them by its name:
You've edited packages.nix, but this output came from the original.
$ nix-build packages.nix -A byethis derivation will be built: /nix/store/4dbnb2x5s0lb1jfc5qbkaidz714kqviy-bye.drvbuilding '/nix/store/4dbnb2x5s0lb1jfc5qbkaidz714kqviy-bye.drv'.../nix/store/56m4lg6glk2rr171wflwb40km215hmmg-bye$ cat resultbye
is exactly such a set, only much bigger. Give
nix-build'<nixpkgs>' as the file and -Ahello, and it builds the hello
package from your machine’s copy of nixpkgs:
$ nix-build '<nixpkgs>' -A hello/nix/store/9lmc8ad19psaj2cpwav6a0yzjqlrn00j-hello-2.12.3$ ./result/bin/helloHello, world!
hello was already in the store, so all it printed is the path.
The quotes around <nixpkgs> keep your shell from reading < and >.
Add --no-out-link when you don’t want the result link. It still prints the
path, but leaves your directory alone. That’s handy in a script, which only
needs the path:
$ nix-build '<nixpkgs>' -A hello --no-out-link/nix/store/9lmc8ad19psaj2cpwav6a0yzjqlrn00j-hello-2.12.3$ $(nix-build '<nixpkgs>' -A hello --no-out-link)/bin/helloHello, world!
Here’s the same bye built both ways:
You've edited packages.nix, but this output came from the original.
Same store path. Only the way you name the file and the package changes.
Need a program for a few minutes, without installing it? nix-shell starts a
Bash with extra on your . It’s
always , even if you normally use another shell. With -p, you list the
packages by their names in :
$ nix-shell -p hello jq[nix-shell:~]$ helloHello, world![nix-shell:~]$ jq --versionjq-1.8.2[nix-shell:~]$ exitexit$ hellobash: hello: command not found
Notice how hello is gone after exit. Meaning it was never installed. The
shell only borrowed it.
To run one command and come straight back, put it after --run:
$ nix-shell -p hello --run helloHello, world!
nix-shell can also sit at the top of a script. Then the script brings its
own tools:
-ibash says which program runs the rest of the file. Whoever runs the script
doesn’t need to install first.
The packages come from the nixpkgs your machine already has, usually a
. So the jq you get depends on when that
channel was last updated.
To pick the nixpkgs yourself, pass it with -I. Point it at one exact commit,
and everyone who runs the line gets the same jq:
$ nix-shell -p jq -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/774debe7a0d1b496e35677ad955a1011c6ff74f3.tar.gz --run "jq --version"unpacking 'https://github.com/NixOS/nixpkgs/archive/774debe7a0d1b496e35677ad955a1011c6ff74f3.tar.gz' into the Git cache...jq-1.8.2
Projects often start their with a of things to install first. A
shell.nix replaces that list. It sits in the project directory, and its
packages list is what the shell gets:
Meaning anyone who clones the project and types nix-shell gets cowsay. Add
the same -I as before, and they get the same cowsay too.
Without -p, nix-shell looks for shell.nix in the current directory, then
default.nix. It stops with an error when there’s neither:
$ nix-shellerror: no argument specified and no 'shell.nix' or 'default.nix' file found in the working directory
Writing an into a file just to see its gets old fast.
nixrepl gives you an interactive prompt instead: type an expression, press
Enter, and it prints the value right away. So whenever you’re unsure how some
piece of Nix behaves, try it here first, without touching any of your real
files. The editable boxes on this map work the same way, just inside your
browser.
The also lets you name a value with name = value. You can use that name
in everything you type afterwards. Here’s a short real session:
$ nix replNix 2.34.8Type :? for help.nix-repl> 2 + 35nix-repl> port = 8080nix-repl> port + 18081nix-repl> :l <nixpkgs>Added 27249 variables.…nix-repl> hello.version"2.12.3"nix-repl> hello.meta.description"Program that produces a familiar, friendly greeting"nix-repl> :q
Notice the lines that start with a colon. They aren’t Nix, they’re commands
for the repl itself. :? lists all of them, and :q quits.
The one you’ll use most is :l<nixpkgs>. <nixpkgs> means the copy of
nixpkgs your system is set up with. :l loads it, so every package becomes a
name you can type, like hello above. That’s where the 27249 variables come
from.
You don’t need to know what’s inside a package to look around. Type
hello.meta. and press Tab. The repl lists everything in hello’s meta, the
set of facts about the package:
Two more commands take you from looking to doing. :ehello opens hello’s
package.nix in your editor, so you can read how it’s made. :bhello builds
it and tells you where it landed in the store:
nix-repl> :b helloThis derivation produced the following outputs: out -> /nix/store/9lmc8ad19psaj2cpwav6a0yzjqlrn00j-hello-2.12.3
Your own works too. :lf. loads the flake in the current directory,
and its become names you can type. On a machine with a flake,
that lets you read any of your system without rebuilding. Say the flake’s
configuration.nix sets networking.hostName="snowflake"; and turns on SSH
with services.openssh.enable=true;:
The file never mentions a port. 22 is the default the SSH picked, and
the repl shows it anyway, because config is the finished system with every
default filled in.
The newer nix commands, like nixbuild and nixrun, and are
, and a fresh install of Nix leaves them switched off.
Using one prints an error that names the missing feature:
$ nix eval --expr '1 + 2'error: experimental Nix feature 'nix-command' is disabled; add '--extra-experimental-features nix-command' to enable it
nix-command switches on the nix subcommands, and flakes switches on
flakes. You almost always want both, because addresses like nixpkgs#hello
need the two together.
Flakes and nix commands are still experimental. Turn them on first:
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Sometimes you want one and no conversation. nixeval a
single , prints the result and exits, which makes it handy in
scripts and for quick checks. With --expr, the expression comes straight
from the command line, so wrap it in single quotes to keep your from
touching it.
It can also read a value straight out of a . nixpkgs#hello names the
hello package in , and .version picks its version:
$ nix eval nixpkgs#hello.version"2.12.3"$ nix eval nixpkgs#hello.meta.description"Program that produces a familiar, friendly greeting"
The version comes back in quotes, because nixeval prints a value the way Nix
would write it. That’s easy for you to read, but a script usually wants just
the text. Add --raw, and you get the bare :
$ nix eval --raw nixpkgs#hello.version2.12.3$ echo "hello is at version $(nix eval --raw nixpkgs#hello.version)"hello is at version 2.12.3
and sets have no bare text form. When another program reads them, add
--json. Here’s the same set both ways:
Notice that Nix plans to fetch five paths and build nothing. Meaning “build”
doesn’t always mean compiling. This exact hello is already on
cache.nixos.org, so Nix just downloads it. The other four paths are
hello needs to run, like .
Now build it for real. nixbuild leaves a called result in the
current directory. It points at the package in the store. Run the program
straight from the link:
nixbuild printed nothing when it finished. The result link is how you
find what it built.
Now run the dry run again. hello is in your store, so there’s nothing left
to do. It prints nothing:
$ nix build nixpkgs#hello --dry-run
Most of the time, though, you build your own project. In a directory with a
, plain nixbuild builds the flake’s default package. Here the default
is a tiny script of your own, called greet:
flake.nixFlakeA flake with inputs and outputs.
You've edited flake.nix, but this output came from the original.
$ nix buildwarning: creating lock file "/home/yurii/my-flake/flake.lock": • Added input 'nixpkgs': 'github:NixOS/nixpkgs/774debe7a0d1b496e35677ad955a1011c6ff74f3?narHash=sha256-nQyFkMR78WP6PiXKkDW2SjqzLf/spQqjRuxRGSbcV8k%3D' (2026-10-02)$ ./result/bin/greetHello from my own flake!
You didn’t name a package here. These three are the same build:
You've edited flake.nix, but this output came from the original.
$ nix build$ nix build .$ nix build .#default
The . is the flake in this directory. The #default picks its default
package. To build another package, put its name after .#:
You've edited flake.nix, but this output came from the original.
$ nix build .#cowsay$ ./result/bin/cowsay hi ____< hi > ---- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || ||
nixpkgs#hello at the top works the same way. nixpkgs is a short name that
Nix looks up in its . Out of the box, it stands for
nixpkgs-unstable.
You’ll also meet projects without a flake. They keep their in a plain
.nix file, and you pass that file with --file. Here’s a tiny
that writes one line into its output:
hello-world.nixNixA plain Nix expression.
You've edited hello-world.nix, but this output came from the original.
$ nix build --file hello-world.nix$ cat resulthello world
In a script, you usually want the itself. --print-out-paths
prints it when the build is done. --no-link skips the result link, so
nothing is left in your directory:
When a build fails, nixbuild stops and shows the end of the build’s log.
Say you make a typo and write $oot instead of $out:
hello-world.nixNixA plain Nix expression.
You've edited hello-world.nix, but this output came from the original.
$ nix build --file hello-world.nixerror: Cannot build '/nix/store/03xwlws9siwmmsw2g2k1zpjhlvsb95jy-hello-world.drv'. Reason: builder failed with exit code 1. Output paths: /nix/store/rlif6968wqr1f6g6mxy7g6kdbg6jv4sa-hello-world Last 1 log lines: > sh: can't create : nonexistent directory For full logs, run: nix log /nix/store/03xwlws9siwmmsw2g2k1zpjhlvsb95jy-hello-world.drv
The line starting with > comes from the build itself. $oot is empty, so
sh tries to write to a file with no name. For the whole log, run the
nixlog command at the end.
While a build runs, nixbuild hides its log. Add -L to see it. Here the
forgets > $out, so helloworld goes to the log instead:
hello-world.nixNixA plain Nix expression.
You've edited hello-world.nix, but this output came from the original.
$ nix build --file hello-world.nix -Lhello-world> hello worlderror: builder for '/nix/store/m8s2qvxd5rag0kr3i2agyyq7ckjidri0-hello-world.drv' failed to produce output path for output 'out' at "/nix/store/m8s2qvxd5rag0kr3i2agyyq7ckjidri0-hello-world.drv.chroot/root/nix/store/wmhy6sa98m5s8gf4m2r5nmkp1v6sa8ky-hello-world"
The hello-world> line is the build’s own output, with the package name in
front. The error says why the build failed. Nothing was
written to $out.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Want to try a tool without installing it? nixshell starts a new with
the you ask for on your . Close that shell, and they’re off
your PATH again, with nothing added to your system’s package .
Each package is written as an address. The part before # says where to get
it, and the part after # says which package:
nixpkgs#jq is from
github:NixOS/nixpkgs/nixos-24.05#jq is jq from the nixos-24.05 on
.#my-tool is my-tool from the in the current directory
For nixpkgs, the name after # is the package’s name, the same one
search.nixos.org shows.
You can ask for several packages at once:
$ jq --versionbash: jq: command not found$ nix shell nixpkgs#hello nixpkgs#jq$ helloHello, world!$ jq --versionjq-1.8.2$ exitexit$ jq --versionbash: jq: command not found
The new shell is your usual one, the one your SHELL variable
names. So your prompt, aliases and completions all come along.
The address decides which version you get. An older branch of nixpkgs has an
older jq:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Sometimes you need a program once, to try it or for a single job. nixrun
runs it straight from a , without installing it.
Give it an address like nixpkgs#hello, which means the hello from
. It fetches or builds the package, runs its program, and drops you
back where you were:
$ nix run nixpkgs#helloHello, world!$ hellobash: hello: command not found
The program ran, but it isn’t installed. Type hello on its own afterwards,
and your can’t find it.
Anything after -- goes to the program instead of to nix:
$ nix run nixpkgs#hello -- --greeting "Hello from nix run"Hello from nix run
You’ll meet this in project too. Many say to try a tool with
nixrungithub:owner/repo. That runs it straight from its , without
cloning anything. nixpkgs itself is a repository on :
$ nix run github:NixOS/nixpkgs#helloHello, world!
A package’s name isn’t always its command’s name. ’s package is called
ripgrep, but its command is rg. nixrun still runs the right one,
because the package says so in meta.mainProgram:
So nixrunnixpkgs#ripgrep runs bin/rg from the package:
$ nix run nixpkgs#ripgrep -- --version | head -1ripgrep 15.1.0
When a package has no meta.mainProgram, nixrun falls back to its pname.
dos2unix is one of those. Its package holds four commands, dos2unix,
mac2unix, unix2dos and unix2mac, and the pname picks dos2unix:
$ nix eval nixpkgs#dos2unix.pname"dos2unix"$ nix run nixpkgs#dos2unix -- --version | head -1dos2unix 7.5.4 (2026-01-30)
That means nixrunnixpkgs#dos2unix always runs dos2unix. To run unix2dos from
the same package, a flake can name it as an app. Here the app to-windows
runs unix2dos, which turns Unix line endings into Windows ones:
flake.nixFlakeA flake with inputs and outputs.
An app is a set with type="app" and a program, the path of the file to
run.
Before it looks for a package, nixrun checks the flake’s apps output for
the name. So in that directory, nixrun.#to-windows runs unix2dos, even
though no package has that name:
You've edited flake.nix, but this output came from the original.
$ nix run .#to-windows -- notes.txtwarning: creating lock file "/home/yurii/my-flake/flake.lock": • Added input 'nixpkgs': 'github:NixOS/nixpkgs/774debe7a0d1b496e35677ad955a1011c6ff74f3?narHash=sha256-nQyFkMR78WP6PiXKkDW2SjqzLf/spQqjRuxRGSbcV8k%3D' (2026-10-02)unix2dos: converting file notes.txt to DOS format...
Leave out the # and the name, and nixrun looks for default instead,
first under apps, then under packages. That’s what a README’s plain
nixrungithub:owner/repo runs. This flake has neither yet.
Try it
Rename to-windows to default, so a plain nix run in this directory runs unix2dos.
Offer an app named default
flake.nixFlakeA flake with inputs and outputs.
is a you configure . Programs, users, ,
the desktop and even the are all set in one file,
/etc/nixos/configuration.nix, and NixOS builds the whole system from it.
On most distributions you set up a machine one command at a time. On Ubuntu,
that could look like this:
sudohostnamectlset-hostnamenixos
sudotimedatectlset-timezoneEurope/Kyiv
sudoaptinstallcups
sudoaptinstallfirefox
A year later nobody remembers which commands ran. On NixOS the same machine is
a few lines in one file, and the installer writes it for you:
Unlike those commands, this file is the record. Copy it to a new machine and
you get the same system.
Each line sets an . In
time.timeZone="Europe/Kyiv";, time.timeZone is the setting and
"Europe/Kyiv" is its . The semicolon ends the line.
Hover a name to read its documentation. To find more options, search
search.nixos.org.
To change the machine, you change a value and run
sudonixos-rebuildswitch. The output is cut down to … here:
$ sudo nixos-rebuild switch[sudo] password for yurii:building the system configuration...…activating the configuration...…Done. The new configuration is /nix/store/…-nixos-system-nixos-26.05.11045.774debe7a0d1
NixOS builds the new system next to the old one and switches to it. The old
one stays in the . Meaning a bad change is always one reboot away
from being undone.
NixOS knows every option it offers and what kind of value each one
takes. So a misspelled name or a wrong value stops the with an error
instead of being quietly ignored. Write programs.firefox.enabel, and the
rebuild stops with “The option `programs.firefox.enabel’ does not exist”.
Try it
This machine has no time zone yet. Add time.timeZone = "Europe/Kyiv"; under the networking.hostName line, then change Europe/Kyiv to your own city, like America/New_York.
Set time.timeZone
configuration.nixNixOS moduleA NixOS module.
This configuration doesn't build. Read the error in the editor, then change enabel to enable.
Set programs.firefox.enable to true
configuration.nixNixOS moduleA NixOS module.
On , your whole system is described in one file,
/etc/nixos/configuration.nix. When you install NixOS, the installer writes
this file for you from the choices you make there, like the language, time
zone, user and desktop. It belongs to , so you edit it with
sudoedit/etc/nixos/configuration.nix.
It’s about 120 lines long, but half of them are comments starting with #.
Here is its top and a few of the :
configuration.nixexcerptNixOS moduleA NixOS module.Project filesA file in the example project.
The {config,pkgs,...}: line is there because configuration.nix is
just a with a set . During a
, NixOS calls it with a set of arguments, config, , and a
few more, and the it returns holds your options. This also
explains the names at the top: asking for pkgs there is how the file gets
access to .
The imports pulls in hardware-configuration.nix, the second file the
installer wrote.
The at the end of the is there because NixOS passes more
arguments than the file asks for. Without it, the rebuild stops with an
unexpected argument error.
Try it
This header has no ..., so NixOS cannot call it with its extra arguments. Change { config, pkgs }: to { config, pkgs, ... }:.
NixOS can call the file
configuration.nixNixOS moduleA NixOS module.
Saving configuration.nix doesn’t change anything on the running machine. The
file is only a description, and nixos-rebuild is the command that turns it
into a system. It reads your and builds what it describes,
downloading ready-made where it can. Then it switches to the new
system.
Most of the time you run sudonixos-rebuildswitch. It builds the new system
and switches to it right away, restarting whatever changed. It also
makes it the default entry in the . The previous system stays in the
boot menu too. Meaning a bad change is one reboot away from undone.
Here it ran with nothing changed since the last build, so there was nothing to
download or build:
$ sudo nixos-rebuild switch[sudo] password for yurii:building the system configuration...Checking switch inhibitors... doneactivating the configuration...setting up /etc...reloading user units for yurii...restarting the following user units: nixos-activation.servicerestarting sysinit-reactivation.targetthe following new units were started: NetworkManager-dispatcher.service, sysinit-reactivation.target, systemd-tmpfiles-resetup.serviceDone. The new configuration is /nix/store/…-nixos-system-nixos-26.05.11045.774debe7a0d1
This time the output first lists what it will build and what it will fetch
from https://cache.nixos.org. Then it copies and builds them. Long lists are
cut down to … here:
$ sudo nixos-rebuild switch[sudo] password for yurii:building the system configuration...these 16 derivations will be built: /nix/store/…-system-path.drv … /nix/store/…-nixos-system-nixos-26.05.11045.774debe7a0d1.drvthese 19 paths will be fetched (38.9 MiB download, 116.4 MiB unpacked): /nix/store/…-git-2.54.0 … /nix/store/…-vim-9.2.0541 …copying path '/nix/store/…-vim-9.2.0541' from 'https://cache.nixos.org'...…copying path '/nix/store/…-git-2.54.0' from 'https://cache.nixos.org'...building '/nix/store/…-system-path.drv'...…building '/nix/store/…-nixos-system-nixos-26.05.11045.774debe7a0d1.drv'...…Done. The new configuration is /nix/store/…-nixos-system-nixos-26.05.11045.774debe7a0d1
Two packages turn into 19 downloads, because brings along the Perl
it needs. The 16 builds are the new system itself, like the list of
installed programs and the files in /etc.
Now make a typo, programs.gti.enable=true;, and run it again:
$ sudo nixos-rebuild switch[sudo] password for yurii:error: … error: The option `programs.gti' does not exist. Definition values: - In `/etc/nixos/configuration.nix': { enable = true; } Did you mean `programs.git' or `programs.mtr'?Command 'nix-build '<nixpkgs/nixos>' --attr config.system.build.nixos-rebuild --no-out-link' returned non-zero exit status 1.
The failed changed nothing. Your running system stays exactly as it was,
because switch only activates a system once it’s fully built.
And a plain rebuild doesn’t update anything. It uses the same
as last time, so your package versions stay
exactly where they were until you update nixpkgs itself. To update, run
sudonixos-rebuildswitch--upgrade. If your system is a , run
nixflakeupdate first instead.
switch has three common siblings:
boot makes the new system the boot default, but keeps the current one running until you reboot. That’s handy for a new , which needs a reboot anyway.
test switches to it right now, but adds no boot entry. Meaning a reboot brings back the previous system.
build only builds it, and leaves a result link in the current directory. It doesn’t even need .
If your system lives in a flake, add --flake and the directory, like
sudonixos-rebuildswitch--flake~/nixos-config.
Say an update broke your Wi-Fi. On you just go back to the system you
had yesterday, because every system you build is kept as a .
Every time nixos-rebuildswitch or boot builds a system that differs from
the last one, NixOS keeps it as a new generation, numbered one after another.
A generation is just a link in /nix/var/nix/profiles named system-1-link,
system-2-link and so on, pointing at that system in the . One more
link there, system, points at the current generation.
Here is a machine that has been rebuilt a few hundred times:
$ ls /nix/var/nix/profiles | tail -3system-573-linksystem-574-linksystem-575-link$ readlink /nix/var/nix/profiles/systemsystem-575-link
nixos-rebuildlist-generations shows the same generations, newest first,
with the date each one was built:
Each generation also shows up in the , so you can pick an older one
when you start the computer.
To go back without rebooting, run sudonixos-rebuildswitch--rollback. It
points system back at the link before it and switches to that system. The
output is cut down to … here:
$ sudo nixos-rebuild switch --rollbackbuilding the system configuration...switching profile from version 575 to 574Checking switch inhibitors... doneactivating the configuration...…$ readlink /nix/var/nix/profiles/systemsystem-574-link
Meaning generation 574 also boots by default from now on.
Rolling back doesn’t delete the newer generation. So the next after a
rollback gets a number above it, not the one after the generation you went
back to.
Old generations stay until you delete them.
sudonix-collect-garbage--delete-older-than30d deletes the ones older than
30 days, except the one that was current back then, and frees the
only they were using.
Generations only cover the system, not your data. The files in your stay as they are, whichever generation you boot.
The first thing you usually do on a fresh system is install some programs. On
, that’s the environment.systemPackages : a
of that every user on the machine gets.
The file the installer writes already has it, commented out. Remove the #
from those lines and it looks like this:
The withpkgs; in front lets you skip the pkgs. prefix. Here’s the plain
list next to the short one:
They’re the same list.
On most you run aptinstallvim, and the system remembers it
somewhere. Here the list is the whole story. Run sudonixos-rebuildswitch,
and vim and wget are in everyone’s . Take a name out, , and
it’s gone again.
The second one also lets NixOS set Git up for you, like the default
name here. So if a program has an enable option, it’s usually the better
choice.
Try it
Add git after wget in the list.
Add git to environment.systemPackages
configuration.nixNixOS moduleA NixOS module.
htop shows what's running on the machine. Add pkgs.htop after pkgs.git. Without with pkgs;, every name needs the pkgs. prefix.
Add htop to environment.systemPackages
configuration.nixNixOS moduleA NixOS module.
On most Linux systems, you add an account with useradd, and it lives in
/etc/passwd. On , you write it in your , and a
creates it. Here’s the account the installer writes for the user you create:
configuration.nixexcerptNixOS moduleA NixOS module.ambient pkgsA piece of a larger file. `pkgs` was bound by the part not shown, so it is supplied here too.
Here’s what each line does:
isNormalUser=true; marks an account for a person. It gets a
in /home.
description is the full name the login screen shows.
extraGroups adds the account to . wheel lets it run commands with
, and networkmanager lets it change network settings.
packages installs programs for this one account alone.
Your username sits inside the name. The option is
users.users, and every account is an under it, named after its login. So a second account is just a
second name:
The installer wrote "yurii" in quotes and this one doesn’t. Both mean the
same thing.
Quotes matter when a name has a dot in it. Without them, the dot starts a new
level. These two lines are not the same:
configuration.nixNixOS moduleA NixOS module.
The first is an account called anna.k. The second is an account called
anna with a k inside it. Accounts have no option called k, so the
rebuild fails:
$ sudo nixos-rebuild switch[sudo] password for yurii:building the system configuration...error: … error: The option `users.users.anna.k' does not exist. Definition values: - In `/etc/nixos/configuration.nix': { isNormalUser = true; } Did you mean `users.users.anna.uid', `users.users.anna.home' or `users.users.anna.name'?…
The password isn’t in the file. The installer set yours. An account you add
later has no password until you give it one with passwd:
$ sudo passwd guest[sudo] password for yurii:New password:Retype new password:passwd: password updated successfully
Each account also has a . It’s unless you pick another one.
shell takes a , so you can pick the itself instead of a path to it:
Rebuild with only that, and NixOS refuses. The message says what’s missing
and how to fix it:
$ sudo nixos-rebuild switch[sudo] password for yurii:building the system configuration...…error: Failed assertions: - users.users.yurii.shell is set to fish, but programs.fish.enable is not true. This will cause the fish shell to lack the basic nix directories in its PATH and might make logging in as that user impossible. You can fix it with: programs.fish.enable = true; If you know what you're doing and you are fine with the behavior, set users.users.yurii.ignoreShellProgramCheck = true; instead.
Meaning a shell needs two lines. One sets it up for the whole system, and one
picks it for your account. Without the first, fish wouldn’t have the Nix
directories in its . So it couldn’t find your programs.
zsh and xonsh get the same check. Bash needs nothing extra, because NixOS sets
it up by default.
Try it
Give anna an account that can use sudo. Add users.users.anna below yurii, with isNormalUser = true; and extraGroups = [ "wheel" ]; inside.
Add a normal account named anna
Add anna to the wheel group
configuration.nixNixOS moduleA NixOS module.
Switch to fish. Change pkgs.bash to pkgs.fish, and add programs.fish.enable = true; above users.users.yurii.
Set users.users.yurii.shell to pkgs.fish
Set programs.fish.enable to true
configuration.nixNixOS moduleA NixOS module.
A is a program that keeps running in the background, like an
server that lets you log in to this machine from another computer. On most
you install it, edit its config file and enable its
unit by hand. On , the file the installer writes already has the
line for it, commented out:
Remove the # in front of services.openssh.enable, , and NixOS does all of it under the hood: it writes
/etc/ssh/sshd_config, creates the sshd system account, opens 22 in
the , and adds sshd.service, which systemd starts right away and on
every boot. Your job is only to choose the end result.
The under services.openssh work together. Here the server moves to
port 2222 and password logins are turned off:
Notice how false became no. Meaning services.openssh.settings is written
straight into sshd_config, in that file’s own format. And ports did more
than the Port line. The firewall now opens 2222 instead of 22.
Most services follow the same pattern, with an enable option and more
options under the same name, which
search.nixos.org lists.
The same pattern runs a home server. Jellyfin is a free media server, and two
lines set it up:
configuration.nixNixOS moduleA NixOS module.
enable creates the jellyfin system account and jellyfin.service, which
starts on every boot. openFirewall opens Jellyfin’s default ports, TCP 8096
and 8920 and UDP 1900 and 7359, so other devices on your network can reach it.
After a rebuild, open port 8096 of that machine in a browser.
Try it
Keep port 22 and also listen on 2222: change [ 22 ] to [ 22 2222 ].
Set services.openssh.ports to [ 22 2222 ]
configuration.nixNixOS moduleA NixOS module.
Stop anyone from logging in as root over SSH. Add services.openssh.settings.PermitRootLogin = "no"; below the enable line.
Set services.openssh.settings.PermitRootLogin to "no"
configuration.nixNixOS moduleA NixOS module.
During installation, scans your machine and writes a second file next to
configuration.nix. It records the your hardware needs, which
get mounted where, and your . This is the one a fresh install
in a virtual machine generated.
The comment at the top says not to edit this file, because running
nixos-generate-config again overwrites it. configuration.nix pulls it in
through imports, so your own changes go there instead.
Each name under fileSystems is a , so "/" is the root partition
and "/boot" the boot partition. A name like / isn’t a plain word, which is
why it’s in quotes.
Partitions are found by UUID, an ID written to each partition when it was
formatted. No other machine has those IDs, so this file only fits the machine
it came from.
That matters when you take your to a new machine. Most of
configuration.nix carries over, but this file doesn’t, and neither does one
copied from someone else’s setup.
So let NixOS scan the new machine instead. If you installed NixOS on it, the
installer already wrote one to /etc/nixos. Otherwise, run this in your
configuration’s directory:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Every and in your comes from , and without
a , which version of nixpkgs that is depends on the machine: whatever
its nixpkgs last downloaded. Build the same configuration.nix on two machines and you can get
two different systems. A flake fixes that by bundling the nixpkgs version with
your configuration.
The flake for a machine is short. It has one , nixpkgs, and one
under nixosConfigurations, named after the machine:
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
nixpkgs.lib.nixosSystem is the that builds a NixOS system. You give
it the system the machine runs on, and modules, the of files to build it
from.
Your configuration.nix goes in there unchanged, so moving to a flake is mostly
a matter of putting flake.nix next to your existing files. The one exception
is anything you import from a channel, like <home-manager/nixos>. A flake
can’t look up channels, so that has to come in as an input instead.
The first build writes the exact nixpkgs revision into flake.lock. Every
build after that uses the same one, until you run nixflakeupdate. So two
machines built from this directory get the same nixpkgs.
To build from the flake, pass its directory to nixos-rebuild with --flake.
Here the flake lives in ~/nixos-config:
$ sudo nixos-rebuild switch --flake ~/nixos-config#snowflake[sudo] password for yurii:building the system configuration...…Done. The new configuration is /nix/store/…-nixos-system-snowflake-26.05.20261002.774debe
The part after # picks the entry from nixosConfigurations. Leave it out, and
nixos-rebuild looks for an entry named after the machine’s current .
Flakes are still marked experimental, but nixos-rebuild turns them on for
its own run.
Try it
The modules list is empty, so this system has none of your settings. Add ./configuration.nix to it.
Build snowflake from ./configuration.nix
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Once you have a desktop and a laptop, most of their is the same:
your user, , . One can hold both machines, and the shared part is
written once.
Each machine gets its own entry under nixosConfigurations. The entry lists
the that machine is built from.
Each machine also gets its own directory under hosts. The shared part goes in
modules/common.nix, and both entries list it:
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
Both machines get the yurii account, Git and SSH from common.nix, and
everything else comes from their own directory. And each
configuration.nix imports the hardware-configuration.nix right next to
it, so a machine only ever gets its own.
That matters, because hardware-configuration.nix describes one specific
machine: its disks, its and the its hardware needs.
A copy from your other machine tells to mount partitions that aren’t
there.
So on every new machine, generate a fresh one into its directory. Run it on the
new machine itself, inside your config directory:
Then gitadd the new directory. A flake only sees files Git tracks. Forget it,
and Nix can’t find the laptop’s files:
$ git status --short?? hosts/laptop/$ nix eval .#nixosConfigurations.laptop.config.networking.hostNameerror: … while calling the 'seq' builtin at «github:NixOS/nixpkgs/774debe7a0d1b496e35677ad955a1011c6ff74f3?narHash=sha256-nQyFkMR78WP6PiXKkDW2SjqzLf/spQqjRuxRGSbcV8k%3D»/lib/modules.nix:402:18: 401| options = checked options; 402| config = checked (removeAttrs config [ "_module" ]); | ^ 403| _module = checked (config._module); … while evaluating a branch condition at «github:NixOS/nixpkgs/774debe7a0d1b496e35677ad955a1011c6ff74f3?narHash=sha256-nQyFkMR78WP6PiXKkDW2SjqzLf/spQqjRuxRGSbcV8k%3D»/lib/modules.nix:305:9: 304| checkUnmatched = 305| if config._module.check && config._module.freeformType == null && merged.unmatchedDefns != [ ] then | ^ 306| let (stack trace truncated; use '--show-trace' to show the full, detailed trace) error: Path 'hosts/laptop/configuration.nix' in the repository "/home/yurii/nixos-config" is not tracked by Git. To make it visible to Nix, run: git -C "/home/yurii/nixos-config" add "hosts/laptop/configuration.nix"$ git add hosts/laptop$ nix eval .#nixosConfigurations.laptop.config.networking.hostNamewarning: Git tree '/home/yurii/nixos-config' is dirty"laptop"
To switch a machine, name its entry after a # in the flake’s directory:
$ sudo nixos-rebuild switch --flake .#laptop
Leave out #laptop, and nixos-rebuild picks the entry named after the
machine’s current . That’s why each host sets networking.hostName
to the name of its entry. A fresh install is still called nixos, so the very
first switch needs the name spelled out.
Try it
Add a third machine. Copy the whole nixosConfigurations.laptop entry and paste it right below. In the copy, change both laptop to server.
Give the server SSH from common.nix
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Almost every NixOS needs this. from , sops-nix or
disko all come from flake , and your configuration.nix has to reach
them somehow.
Say configuration.nix wants to import Home Manager’s module, the one
that lets your system set up your too. That module
lives in a flake input, and modules don’t get your inputs. The
hands every module like config, and , and your flake’s
inputs aren’t among them.
Ask for inputs anyway, and see what happens. This configuration.nix wants
inputs, but the flake never hands them over:
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
, and stops:
$ sudo nixos-rebuild switch --flake .#snowflake[sudo] password for yurii:… … while evaluating the module argument `inputs' in "/nix/store/v7g97wamjg6mffa22fps892xak9kajrj-source/configuration.nix": … noting that argument `inputs` is not externally provided, so querying `_module.args` instead, requiring `config` … if you get an infinite recursion here, you probably reference `config` in `imports`. If you are trying to achieve a conditional import behavior dependent on `config`, consider importing unconditionally, and using `mkEnableOption` and `mkIf` to control its effect. (stack trace truncated; use '--show-trace' to show the full, detailed trace) error: infinite recursion encounteredCommand 'nix --extra-experimental-features 'nix-command flakes' build --print-out-paths '.#nixosConfigurations."snowflake".config.system.build.nixos-rebuild' --no-link' returned non-zero exit status 1.
The line with “noting that” says why. Nobody handed inputs to the module, so
Nix looks for it in config instead. And config can’t be ready while
imports is still being worked out. The hint below that line is about a
different mistake, so you can skip it.
is how you provide it. It’s a set you give to nixosSystem, and
every name in it becomes an argument that any module can ask for. The usual
move is to hand over all inputs at once:
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
inherit inputs; is short for inputs=inputs;. Now every module can write
inputs in its and reach any input from there.
Many flakes unpack the inputs with {nixpkgs,...}@inputs:. The @inputs
keeps the whole set under the name inputs too, so inherit inputs; still
works.
It even works right inside imports, because names from specialArgs
are there from the start.
Home Manager modules are a module system of their own, so they don’t see
these arguments. Under NixOS, you pass them on once more, next to your Home
Manager user:
configuration.nixexcerptNixOS moduleA NixOS module.Project filesA file in the example project.
Now home.nix can start with {inputs,...}: too.
Try it
specialArgs can carry any value, not only inputs. configuration.nix also asks for username. Add username = "yurii"; inside specialArgs, after inherit inputs;.
Hand username to every module, so the yurii account exists
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
Your desktop, your browser and every game draw with your , and the GPU
needs for that. On they come from one ,
hardware.graphics.enable. It installs Mesa, the open drivers most GPUs run on.
You’ll rarely write that line yourself. A login screen like GDM, or a like Hyprland, turns it on for you.
Got an AMD or Intel GPU? Then you’re done. Their drivers are already
part of Linux, and Mesa does the rest.
NVIDIA is the odd one out. Out of the box you get nouveau, an open driver
that works, but it’s slow in games and can’t run CUDA. For those you want
NVIDIA’s own driver, and that takes three lines:
configuration.nixNixOS moduleA NixOS module.
services.xserver.videoDrivers is the switch. Don’t let the xserver fool
you, it works for desktops too.
nixpkgs.config.allowUnfree is there because NVIDIA’s driver is .
Leave it out, and the stops and tells you to add it. Here’s that
error, with the middle cut down to …:
error: Refusing to evaluate package 'nvidia-x11-595.71.05' in /nix/store/3zvg83mg9aavm9bgh26ydljchply7i25-source/pkgs/os-specific/linux/nvidia-x11/generic.nix:317 because it has an unfree license (‘unfreeRedistributable’)…b) For `nixos-rebuild` you can set { nixpkgs.config.allowUnfree = true; }in configuration.nix to override this.
hardware.nvidia.open picks between NVIDIA’s two kernel drivers, an open one
and a closed one. The open one only works on RTX and GTX 16xx cards, and NixOS
makes you choose. Leave it out, and the rebuild stops with expectedaBooleanbutfoundnull.
Older cards have one more catch. By default NixOS installs NVIDIA’s 595 driver,
and NVIDIA stopped supporting GTX 9xx and 10xx cards after its 580 driver.
hardware.nvidia.branch asks for that older driver instead.
So it comes down to your card:
RTX cards and GTX 16xx: open=true.
GTX 9xx and 10xx: open=false, and branch="legacy_580".
After a rebuild and a reboot, nvidia-smi shows your card and the driver
version. That’s how you know NVIDIA’s driver is the one running.
Some programs, like Wine and many older games, are 32-bit and need 32-bit
drivers. hardware.graphics.enable32Bit installs them. If you play through
Steam, programs.steam.enable=true; turns it on for you.
Try it
This machine has a GTX 1080. Set up its driver the way the list above says, under the videoDrivers line.
Set hardware.nvidia.open to false
Set hardware.nvidia.branch to "legacy_580"
configuration.nixNixOS moduleA NixOS module.
This machine also plays games through Steam. Turn Steam on.
Set programs.steam.enable to true
configuration.nixNixOS moduleA NixOS module.
GNOME and Hyprland both give you windows on a screen, but they’re very
different things. GNOME is a . It’s the whole graphical
session, with its own panel, settings app, file manager and more. Hyprland is a
. It only arranges your windows, as tiles side by side, and
leaves the rest to you. KDE Plasma is a desktop environment too, and niri is a
window manager like Hyprland.
On , you switch each one on with an or two. When you pick
GNOME, the installer writes these lines:
services.desktopManager.gnome.enable installs GNOME, and
services.displayManager.gdm.enable turns on GDM, the login screen that starts
it. For Plasma, the installer writes services.displayManager.sddm.enable and
services.desktopManager.plasma6.enable instead.
Wayland window managers like Hyprland and niri live under programs instead.
Here’s Hyprland added next to GNOME:
A configuration.nix, a home.nix and a nix-darwin
all look alike, and that’s because they’re the same kind of
file: a . A module is just an of settings, like
programs.git.enable=true;, or a that returns one.
So you’re not stuck with one big file. Once it grows, you split it into
modules, one for your editor, one for your desktop, one for a web server. The
, which ships with , reads all of them and combines them
into one configuration.
Most modules are functions, because they need something from outside, usually
to name . The module system calls the function with one set that holds
config, , pkgs and a few more, and the module picks out the ones it uses.
editor.nixNixOS moduleA NixOS module.
Notice the in the . It’s there because the set has more names than
any module asks for, and without it NixOS stops with “called with unexpected
argument”.
Try it
This module is a plain set, so it cannot use pkgs yet. Add { pkgs, ... }: above its opening brace, then put pkgs.git inside the empty list.
Ask for pkgs and add pkgs.git to environment.systemPackages
tools.nixNixOS moduleA NixOS module.
A line like programs.git.enable=true; in a or Home Manager
sets an . An option is a setting that some declared
in advance, with a name, a that says which it accepts,
usually a default, and a description.
Setting an option doesn’t create anything. You give a value to an option that
already exists, and every option you don’t mention keeps its default. NixOS
and each come with thousands of them.
Hover an option’s name in the editor below to see its type, default and
description. For NixOS, search.nixos.org
shows the same for every option.
configuration.nixNixOS moduleA NixOS module.
Mistakes stop you before anything gets built, because every
option and its type are known in advance. A typo like programs.gti.enable
fails with “The option `programs.gti’ does not exist”, and "yes" where a
belongs fails with “is not of type `boolean’”.
Try it
networking.firewall.allowedTCPPorts is declared as a list of ports. Open port 80 for a web server: add networking.firewall.allowedTCPPorts = [ 80 ]; under the Git line.
Open port 80 in networking.firewall.allowedTCPPorts
configuration.nixNixOS moduleA NixOS module.
Say one file sets the , and another wants to print it in the login
message. So the second file needs to read an , not only set one.
That’s what the config is for. Add it to a ’s ,
and it holds the final of every option. It’s the result after all
modules have been combined. So a module can use a choice made in another file
without knowing which file made it.
Here motd.nix builds the login message from the host name that host.nix
sets:
motd.nixhost.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
Notice how motd.nix comes first, yet it reads a value that host.nix sets.
Meaning the order of modules doesn’t matter, because config is the result of
all of them together.
It may look odd that a module reads a result it’s part of. It works because
Nix is : it only works out a value when something
asks for it. The hands every module the finished config up
front, and each value inside gets computed on first use.
’s own modules do this all the time. This is a piece of the module.
It reads the you chose for SSH, wherever you chose them, and opens
exactly those in the :
cfg is just a short name for config.services.openssh. And cfg.ports is
whatever any module set services.openssh.ports to.
lib.optionals gives back the ports only when cfg.openFirewall is true.
Otherwise it gives an empty . lib.mkIfcfg.enable makes all of it apply only
while SSH is on.
The config = inside is a different thing from the argument. It’s where the
module sets options. The config on the first line is where it reads them.
What breaks is a value that depends on itself. Here host.nix
sets the host name to the host name. To know it, Nix has to know it first:
motd.nixhost.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
The error says . To fix it, the host name has to come from
somewhere else, like a plain "laptop".
Try it
Make motd.nix show the time zone that host.nix sets. Add { config, ... }: above its opening brace, then add + " (" + config.time.timeZone + ")" after "Welcome".
motd.nixhost.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
If you use , you’ve already seen imports at the top
of configuration.nix: imports=[./hardware-configuration.nix];. It’s how
find each other. It’s a of to other module
files, and every module in it joins the same as the module that
lists it.
Here configuration.nix imports two small modules. editors.nix adds Vim to
environment.systemPackages, and tools.nix adds and :
configuration.niximports the rest
the files it imports
editors.niximportedtools.niximported
merge enabled modules
merged configuration
Nothing is imported, so there is nothing to build.
All three end up in the result. Every imported module
adds its settings to the same pile, and the merges them. Take
./tools.nix out of the list, and Git and ripgrep leave the result.
The paths are ordinary Nix paths, so they’re relative to the file that lists
them. Say your config is split into directories like this:
Then laptop/configuration.nix imports both like this:
laptop/configuration.nixexcerptNixOS moduleA NixOS module.Project filesA file in the example project.
./ starts in the laptop directory, where this file is. ../ steps up one
directory, into nixos-config.
Don’t mix it up with . import./desktop.nix just reads the file and
gives you back what’s in it:
desktop.nixread only
You get back a plain set, and nothing changes in your config. imports is
what makes the file part of it.
Not every entry has to be a path. Most NixOS add-ons come with a module of
their own, and you import it the same way. Here ’s module comes
from the ’s :
configuration.nixexcerptNixOS moduleA NixOS module.Project filesA file in the example project.
The second entry is not a path. It’s a module someone else wrote, and it
joins the same pile as your own files. That’s how Home Manager, disko
and most other NixOS add-ons get into a config.
If you use flakes, inputs comes from your flake.nix. The flake has to hand
it to your modules with specialArgs={inherit inputs;};.
An imported module can have its own imports. So one entry file can pull in a
whole tree of modules.
A file imported from several places still counts once. Here
desktop.nix and printing.nix both import ./users.nix, which puts yurii
in the wheel . configuration.nix imports it too:
Nothing is imported, so there is nothing to build.
Three modules adding to the same list would give you wheel three times, like
the packages above. But users.nix is the same file every time, so it joins
once.
Try it
ssh.nix sits next to configuration.nix, but nothing imports it yet. Add ./ssh.nix to the end of the imports list.
Import ./ssh.nix, so its services.openssh.enable reaches the configuration
configuration.nixNixOS moduleA NixOS module.Project filesA file in the example project.
Once your is split into several files, they start touching the
same . editors.nix wants Vim installed, and tools.nix wants and
. Both set environment.systemPackages, and neither has to know about
the other, because the merges their :
editors.nixtools.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
join, so all three end up installed. Switch a off in its
and its packages leave the result.
does this too. programs.git.enable=true; turns on a module inside
. That module adds Git to the same environment.systemPackages list as
your files. This is the part of it that does that:
Sets merge name by name. Here each file adds one , and
both end up in environment.variables:
editors.nixtools.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
A setting like networking.hostName holds one value, so there’s nothing to
join. Two equal definitions are fine. Two different ones stop the :
base.nixserver.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
So a disagreement shows up before anything gets built, and Nix never guesses
which file you meant. In a real , the error also suggests
lib.mkForce. That lets one file win on purpose.
The option decides which of these happens. Its declaration gives it
a : environment.systemPackages is a list of packages,
environment.variables is a set, and networking.hostName is a . Meaning
the same = joins, merges or conflicts depending on what the option holds.
Try it
Both files set EDITOR. Change it to "vim" in tools.nix, and read what the module system says.
editors.nixtools.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
Let server.nix win on purpose. Change "web" to lib.mkForce "web".
base.nixserver.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
Sometimes two disagree on purpose. A module shared by all your machines
suggests a , and one machine wants its own. With two plain definitions,
that’s a conflict:
profile.nixhost.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
Wrap the suggestion in lib.mkDefault, and it gives way:
profile.nixhost.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
Sometimes the module you disagree with isn’t yours, and it didn’t use
mkDefault. Say a shared desktop.nix turns on the , and this machine
wants it off. Wrap your own value in lib.mkForce, and it wins:
desktop.nixhost.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
That works because every definition has a number, and the lower
number wins:
lib.mkForce gives 50
a plain definition gets 100
lib.mkDefault gives 1000
the ’s own default sits at 1500
So mkForce beats a plain definition, and a plain definition beats
mkDefault. All three replace the option’s default.
A mkDefault never joins a fight between plain definitions. Here only
server.nix and host.nix clash:
profile.nixserver.nixhost.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
mkDefault and mkForce don’t decide anything by themselves.
They only wrap the value with its priority number. The reads that
number later, while merging:
You’ll find mkDefault on your own machine too. If you use , your
hardware-configuration.nix has this line:
The installer only suggests a platform. Your own can still
replace it without a conflict.
Try it
NixOS's minimal profile turns off logrotate, the service that cleans up old log files, with mkDefault. Turn it back on. In configuration.nix, add services.logrotate.enable = true;.
minimal.nixconfiguration.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
Both files only suggest a value, so they tie at 1000 and clash. In laptop.nix, change lib.mkDefault false to a plain false.
common.nixlaptop.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
Sometimes a setting should only apply in some cases. Say you want fail2ban,
which blocks addresses that keep failing to log in, but only while is on.
The obvious way is to copy SSH’s switch:
ssh.nixNixOS moduleA NixOS module.
With SSH off, this line doesn’t stay quiet. It says false, and
that’s a definition like any other. So if another turns fail2ban on,
the two disagree, and the stops with “The option
`services.fail2ban.enable’ has conflicting definition values”.
What you meant was “only when SSH is on, and otherwise say nothing”. That’s
exactly what lib.mkIf says:
ssh.nixNixOS moduleA NixOS module.
With SSH off, it’s as if this module never set the . fail2ban keeps its
default, or whatever other modules set, and nothing clashes.
So how can a be “nothing”? mkIf doesn’t decide anything by itself. It
just packs the condition and the value into a set:
The opens that set later, while merging, and skips it when the
condition is false. It only looks inside once every module has been read, so
the condition can safely read config.
This is how almost every in is written. Here is the module for
the Orca screen reader from :
Meaning services.orca.enable=true; in your is the switch.
Until you flip it, those four lines don’t exist.
Try it
Open port 22 only while SSH is on. Replace false after lib.mkIf with config.services.openssh.enable.
Add 22 to networking.firewall.allowedTCPPorts only when config.services.openssh.enable is true
firewall.nixNixOS moduleA NixOS module.
Once your is split into , it’s tempting to import some of
them only on certain machines. Say gaming.nix turns on Steam, and you only
want it where there’s a desktop. So you make the import depend on an :
configuration.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
Nothing comes out, not even the . stops with infinite recursion encountered.
config is the finished configuration, and NixOS builds it out of every module
it imports. To know what config.services.displayManager.enable
is, it has to read all the modules first, but to know which modules there are,
it has to read config. Each one waits for the other. That’s why, on a real
machine, the full error comes with a hint: “if you get an infinite recursion
here, you probably reference config in imports”.
The fix is the one that hint suggests. Import the module everywhere, and give
it its own switch. lib.mkEnableOption declares a true or false option
that starts out false, and lib.mkIf only applies the settings when it’s on.
Now every machine imports gaming.nix, and the ones that want it say so in
their own configuration:
configuration.niximports the rest
the files it imports
gaming.niximportedworkstation.niximported
merge enabled modules
merged configuration
Nothing is imported, so there is nothing to build.
This time it works. The host name is back, and Steam is on.
You may still want Steam on every machine with a desktop. It’s tempting to
wrap the whole module in an on config for that:
gaming.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
It stops with the same . To know what this module sets,
NixOS has to run the if. The if needs config, and config needs this
module. lib.mkIf is the fix there too. It hands the its settings right
away, and only checks the condition later, once config is ready. It doesn’t
need an else either.
Try it
Fix gaming.nix. Change if to lib.mkIf, delete then, and delete else { }.
gaming.nixdesktop.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
Every you set was declared by some first. ’s module,
for example, declares services.openssh.enable and services.openssh.ports
in its own file, under options. Each one is a lib.mkOption call with a
set that describes the setting: a type for the it accepts, a
default for when nobody sets it, and a description for the documentation.
sshd.nixexcerptNixOS moduleA NixOS module.
Your own modules can declare options the same way. The most common one is an
on/off switch like enable above, so lib.mkEnableOption writes it for you:
a , false by default, with a description that reads “Whether to
enable” followed by your words. That’s how a module of yours gets turned on
with one line from another file, just like SSH.
Here’s what it writes for you, spelled out. The example is shown in the
docs next to the default:
desktop.nixexcerptambient libA piece of a larger file. `lib` was bound by the part not shown, so it is supplied here too.
And here’s the short way:
desktop.nixexcerptambient libA piece of a larger file. `lib` was bound by the part not shown, so it is supplied here too.
These two are the same option.
Options aren’t only on/off switches. This module declares which browser to
install, and installs it:
desktop.nixNixOS moduleA NixOS module.
The option’s is package, so its value is a itself, like
pkgs.firefox. The module puts that value straight into the .
This time environment.systemPackages sits under config. In a
module with options, Nix needs to know which names you declare and which
ones you set. So its own settings have to go under config. Leave one at the
top, next to options, and the stops:
desktop.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
The error names the setting, and even tells you the fix. It says to move it
“into the explicit `config’ attribute”.
With config in place, Firefox gets installed unless another file says
otherwise. Here laptop.nix says otherwise:
desktop.nixlaptop.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
The result has Chromium in it, and Firefox is gone. The default only
counts when nobody sets the option.
The type keeps out values that make no sense. Set the browser to 5, and the
evaluation stops:
desktop.nixlaptop.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
This time Nix tells you which option got a bad value, and what type it
expected:
A definition for option `desktopApps.browser' is not of type `package'.
Try it
Add a switch for games. Under options, after the browser option, add desktopApps.games.enable = lib.mkEnableOption "games";.
Declare desktopApps.games.enable with lib.mkEnableOption "games"
desktop.nixNixOS moduleA NixOS module.
Add an editor option. Under options, add desktopApps.editor = lib.mkOption { type = lib.types.package; default = pkgs.neovim; };. Then add config.desktopApps.editor to the environment.systemPackages list.
Neovim is installed
desktop.nixNixOS moduleA NixOS module.
The type in an ’s declaration says which the option accepts.
services.openssh.ports is a of lib.types.port, and a is a whole
number from 0 to 65535. So [2222] is fine, but a typo like [99999]
stops the instead of becoming an server that never starts.
The also decides what happens when several set the same option.
networking.extraHosts has the type lines, which joins with a newline
in between. So both modules below get their line into /etc/hosts:
nas.nixprinter.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
i18n.defaultLocale is a plain str, and a system can only have one default
language. Two different values can’t be joined, so the evaluation stops and
names both of them:
base.nixgerman.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
A listOf joins the lists instead. That’s how every module can add its own
to environment.systemPackages, and all of them get installed:
editors.nixtools.nix
merge enabled modules
merged configuration
Every module is switched off, so there is nothing to build.
Types are built from pieces. bool, int, str and port stand on their
own. listOf, attrsOf and nullOr take another type and wrap it, and you
can wrap as deep as you need.
Every type also has a description, a name for people to read. It’s the type
line you see on search.nixos.org. Ask a wrapped type for it, and you can read
the pieces right back:
Meaning an option of this type takes either or a list of strings. A
port reads longer, because its description also gives the range:
Try it
lib.types.int would also accept 99999, which no port can be. Change lib.types.int to lib.types.port.
Declare web.port with lib.types.port, so 8080 passes and 99999 fails
web.nixNixOS moduleA NixOS module.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Your system is only half of your setup. The other half lives in your : your , your name and email, your editor settings, and the
pile of you copy by hand every time you get a new machine.
lets you describe that half in Nix. Your user’s and
program settings go in a , usually called home.nix. Then
home-managerswitch builds it and links the results into your home directory.
You don’t need for it. It runs on any Linux with Nix and on macOS. On
NixOS it can also be part of your system .
To start, run this. It writes a starter home.nix and a flake.nix into
~/.config/home-manager, and switches to it:
$ nix run home-manager/release-26.05 -- init --switch
Here’s a small home.nix with one package and Git set up:
home.nixHome ManagerA Home Manager module.
home.packages is the of packages for your user only.
home.username and home.homeDirectory tell Home Manager whose home directory
this is, so put your own name there.
programs.home-manager.enable installs the home-manager command itself. From
then on, you just run home-managerswitch.
home.stateVersion is the Home Manager release you started with. Some changes
in newer releases only switch on once this number is new enough, so your setup
keeps working the way it did. Not every change waits for it, so skim the
release notes when you update. Set it once and leave it, even after you update.
Run home-managerswitch, and look at what it made:
You've edited home.nix, but this output came from the original.
$ which fzf/home/yurii/.nix-profile/bin/fzf$ cat ~/.config/git/config[user] email = "yurii@example.com" name = "Yurii"$ readlink ~/.config/git/config/nix/store/ll17a5snqmxfc09p60vbc9illjyix15m-home-manager-files/.config/git/config
comes from your profile in your home directory, not from ,
so installing it didn’t need .
The Git config is a link into the . Home
Manager built every file in the store first, then linked each one into place.
Meaning your dotfiles now come from home.nix, and on a new machine one
home-managerswitch puts all of them back.
If you already had that file, the first switch stops, because the old file is
in the way:
$ home-manager switch…Existing file '/home/yurii/.config/git/config' would be clobbered
Run home-managerswitch-bbackup instead. Home Manager renames your old
file to config.backup, and then links the new one.
Try it
ripgrep searches through files. Add pkgs.ripgrep after pkgs.fzf in home.packages.
Add pkgs.ripgrep to home.packages
home.nixHome ManagerA Home Manager module.
bat is cat with syntax colors, and Home Manager has a module for it. Below the home.packages line, add programs.bat.enable = true;.
Turn on programs.bat
home.nixHome ManagerA Home Manager module.
Installing a program is the easy part. Configuring it usually means a
in a format of its own: INI-style sections for , command line flags for
bat, for .
Adding pkgs.git to home.packages installs Git and nothing else.
programs.git.enable=true; does more: it installs Git and lets
write Git’s config file from in your home.nix. You describe the
settings in Nix, and Home Manager handles the syntax. It has like this
for hundreds of programs, all under programs.
home.nixexcerptHome ManagerA Home Manager module.
After a switch, Home Manager has written ~/.config/git/config in Git’s own
format:
$ cat ~/.config/git/config[user] email = "338125742+yurii@users.noreply.github.com" name = "Yurii"
Starship wants TOML instead, and its module takes the same kind of Nix set:
Notice how directory.truncation_length turned into a [directory] section,
just like user.name turned into [user] for Git. You write one shape in Nix,
and each module turns it into whatever its program reads.
These files are links into the read-only
, so editing them by hand won’t work. The
next change goes into home.nix, and the next switch writes a new file.
So if there is a programs module for something, it’s usually a better choice
than a plain . The Home Manager manual lists every module and its
options.
Try it
Give new repositories a main branch. Add init.defaultBranch = "main"; inside settings, under the email line.
Set programs.git.settings.init.defaultBranch to "main"
home.nixexcerptHome ManagerA Home Manager module.
Not every program has a Home Manager , and sometimes you just want a
config file exactly as you wrote it. home.file puts any file anywhere in
your .
Each name under home.file is a path inside your home directory. Its says
where the content comes from. Write the content right in Nix with text, or
take an existing file or directory with source:
home.nixexcerptHome ManagerA Home Manager module.Project filesA file in the example project.
The first line makes an empty ~/.hushlogin, which tells many Linux systems to
skip the welcome message when you log in. The second puts the nvim directory
that sits next to your home.nix at ~/.config/nvim, with everything inside
it.
You’ll also see a shorter form for files in ~/.config. These two do the same
thing:
home.nixexcerptHome ManagerA Home Manager module.Project filesA file in the example project.
home.nixexcerptHome ManagerA Home Manager module.Project filesA file in the example project.
xdg.configFile is just home.file that starts in ~/.config. ’s own modules use it too. programs.git, for example, writes your
settings to xdg.configFile."git/config".
The names are in quotes because without them, a dot starts a new name
inside the one before. Here is the same name written both ways:
The quoted "init.lua" stays one name. The bare init.lua turns into lua
inside init.
A name that starts with a dot is worse. Without quotes, Nix can’t even read
the file:
Names with a slash in them, like ".config/nvim", need quotes too.
Home Manager writes each file into the ,
and puts a to it at that path. After a
switch:
$ ls -l ~/.hushloginlrwxrwxrwx 1 yurii users 73 Oct 11 01:01 /home/yurii/.hushlogin -> /nix/store/dnnzk6z4kgnw5shk7l55iqm08fkw611b-home-manager-files/.hushlogin$ ls ~/.config/nviminit.lua lua$ ls -lL ~/.hushlogin-r--r--r-- 1 root root 0 Jan 1 1970 /home/yurii/.hushlogin
-L follows the link to the file itself. It shows r-- with no w, and the
file belongs to root. Meaning the file is read only. To change it, you change
home.nix and switch again.
Home Manager also won’t overwrite a file you made yourself, unless you tell it
to. Say you already
had your own ~/.config/nvim before you added it to home.nix. The switch
stops, and your directory stays as it was:
$ home-manager switch…Please do one of the following:- In standalone mode, use 'home-manager switch -b backup' to back up files automatically.- When used as a NixOS or nix-darwin module, set either - 'home-manager.backupFileExtension', or - 'home-manager.backupCommand', to move the file to a new location in the same directory, or run a custom command.- Set 'force = true' on the related file options to forcefully overwrite the files below. eg. 'xdg.configFile."mimeapps.list".force = true'Existing file '/home/yurii/.config/nvim' would be clobbered
To get past it, move your directory away and switch again. Or switch with
home-managerswitch-bbackup. Home Manager then renames your directory to
nvim.backup for you.
Try it
nano reads its settings from ~/.nanorc. Turn on line numbers: add home.file.".nanorc".text = "set linenumbers"; under the .hushlogin line.
Write set linenumbers to ~/.nanorc with home.file
home.nixexcerptHome ManagerA Home Manager module.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
can run on its own, with its own home-manager command. Then your
system and your home update in two separate steps, and each keeps its own
history. Meaning rolling back the system leaves your home where it was.
On you can build your home together with the system instead. Then one
nixos-rebuildswitch applies the system and every home in it. Roll the
system back, and your home goes back with it.
Home Manager ships a NixOS for that,
home-manager.nixosModules.home-manager. Put it in your ’s modules
next to ./configuration.nix. Then each user’s home goes under
home-manager.users:
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
Here yurii’s home comes from home.nix, the same kind of file a standalone
Home Manager uses. You can even leave out home.username and
home.homeDirectory, because the NixOS module fills them in from
users.users.yurii.
The home is now part of the system. One switch starts a that sets up
yurii’s home. yurii’s config is then a link into the store:
$ sudo nixos-rebuild switch --flake .#snowflake[sudo] password for yurii:building the system configuration...…activating the configuration...…the following new units were started: home-manager-yurii.service, …$ readlink ~/.config/git/config/nix/store/3gnfpkwdxa52fwmj7p9556b6gmfqihl5-home-manager-files/.config/git/config
useGlobalPkgs=true; gives your home the same as the rest of the
machine. Say your system allows unfree , and your home wants Discord:
With useGlobalPkgs, this builds. Without it, Home Manager imports a
second time just for your home. That copy never sees allowUnfree, so the
stops:
$ sudo nixos-rebuild switch --flake .#snowflake[sudo] password for yurii:building the system configuration...error: … error: Refusing to evaluate package 'discord-1.0.158' in /nix/store/…/discord/default.nix:70 because it has an unfree license (‘unfree’)…Command 'nix --extra-experimental-features 'nix-command flakes' build --print-out-paths '.#nixosConfigurations."snowflake".config.system.build.toplevel' --no-link' returned non-zero exit status 1.
Turn on useUserPackages, and your home’s packages come with the system.
nixos-rebuild installs them through users.users.yurii.packages. Ask the
flake for that list, and your whole home shows up in it as one package:
Turn useUserPackages off, and that list is empty. Home Manager then installs
your packages into ~/.nix-profile instead. That’s one more place to keep
track of.
Home Manager still can’t do what needs . Say you want fish as
your . Enabling fish
in home.nix installs fish and writes its , but it can’t make
fish your login shell:
home.nixexcerptHome ManagerA Home Manager module.
Making it your login shell changes the system’s list of users, so that part
goes in configuration.nix. Set only the there, and NixOS stops the
rebuild:
$ sudo nixos-rebuild switch --flake .#snowflake[sudo] password for yurii:building the system configuration...…error: Failed assertions: - users.users.yurii.shell is set to fish, but programs.fish.enable is not true. This will cause the fish shell to lack the basic nix directories in its PATH and might make logging in as that user impossible. You can fix it with: programs.fish.enable = true; If you know what you're doing and you are fine with the behavior, set users.users.yurii.ignoreShellProgramCheck = true; instead.
anna has an account now, and an anna.nix next to home.nix. Under yurii's line, add home-manager.users.anna = import ./anna.nix;.
Build anna's home from anna.nix, so anna gets fish
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
When runs as a NixOS , your home and your system are built
together, and sometimes the home needs to know what the system did. Say one
home.nix is shared by your desktop and your server. fish is installed on the
desktop but not on the server, and you only want fish’s where
fish actually is.
For that, Home Manager passes every home module an extra , osConfig.
It’s the config: the final of every system . Add it to the
of home.nix, and you can read any NixOS option from there:
home.nixHome ManagerA Home Manager module.
Now the fish configuration in your home follows the system. The desktop’s
configuration.nix has programs.fish.enable=true;, so your home configures
fish there. The server’s doesn’t, so your home leaves fish alone.
The machine’s is a NixOS option too, networking.hostName. Meaning
one home.nix can tell your machines apart. Here your get your work
email on the work laptop, and your own email everywhere else:
home.nixHome ManagerA Home Manager module.
Note that osConfig only reads. To change a NixOS option, you still set it
on the NixOS side. And it’s only filled in when Home Manager is part of NixOS.
In standalone Home Manager, osConfig is , so reading an option
from it stops with expected a set but found null: null.
Set up your home with the same rebuild that sets up your system.
Takes about 30 minutes.
You’ll:
add to your flake
write a home.nix with fzf and your Git name and email
<nixpkgs> means “whatever nixpkgs this machine points to”, which is usually
a . And every machine has its own. Here’s the same file on two laptops.
On one with this year’s nixpkgs:
The exact same shell, two different Pythons. Meaning a project that works on
your laptop can break on your friend’s, or even on your own the moment you
decide to update your NixOS.
A fixes that. It’s just a directory with a flake.nix file in it.
Let’s build one up from nothing. A very basic flake could look like this:
flake.nixFlakeA flake with inputs and outputs.
A flake says two things. inputs is the stuff it wants to download from the
internet. outputs is any Nix code you want it to give back. Here, the flake
downloads nothing and gives back a number.
Don’t mind the inputs: in front of the braces for now. We’ll get to it as soon
as there’s something to download.
To get that number, ask for it with nixeval. The . means the flake in this
directory, and the part after # picks one of its outputs:
$ nix eval .#67
Now let’s download something. Here’s the same flake with one input, nixpkgs:
flake.nixFlakeA flake with inputs and outputs.
Each input gets a name you pick, here nixpkgs, and a url that says where
to download it from. This one is the nixos-26.05 of nixpkgs on
GitHub.
Still 67, but look at the warning. Nix downloaded nixpkgs and made a second
file next to flake.nix, called flake.lock. That long line is what it wrote
in it: the exact commit of nixpkgs it got, 774debe, from October 2.
That’s the lock. Your flake.nix only asks for a branch, and branches keep
moving. The lock remembers which commit you actually got, and from now on Nix
uses that one. Share flake.lock along with flake.nix, and everyone who uses
your flake gets that same commit too. If you’ve used npm, it’s the same idea as
package.json and package-lock.json.
Now let’s use what we downloaded. Remember the inputs: in front of outputs?
That’s because outputs is a . Nix downloads every input first,
then calls outputs with all of them, each under the name you picked. So
inputs.nixpkgs is the whole of nixpkgs, and inputs.nixpkgs.lib is its
of handy functions. toUpper is one of them, and it turns text into
capitals:
flake.nixFlakeA flake with inputs and outputs.
$ nix eval .#"HELLO FROM NIXPKGS"
Now our Python shell. Let’s put it in the flake as an output called myshell:
flake.nixFlakeA flake with inputs and outputs.
The mkShell part is copied straight from shell.nix, and so is the pkgs
line, almost. importinputs.nixpkgs loads nixpkgs and gives back its
packages, like import<nixpkgs> did. Only now it’s the nixpkgs from our
input, at the commit in the lock.
The one new part is system="x86_64-linux". Inside a flake, Nix doesn’t look
at the machine it runs on, so you tell nixpkgs which kind of machine you want
packages for. x86_64-linux means ordinary 64-bit Linux PCs.
Now step into the shell with nixdevelop:
$ nix developerror: flake 'path:/home/yurii/my-flake' does not provide attribute 'devShells.x86_64-linux.default', 'devShell.x86_64-linux', 'packages.x86_64-linux.default' or 'defaultPackage.x86_64-linux'
Nix can’t find our shell. It even says where it looked, right in the error.
The first spot it names, devShells.x86_64-linux.default, is where
nixdevelop expects a shell. It’s just this command’s convention:
devShells, then your system, then default.
So let’s move our shell there:
flake.nixFlakeA flake with inputs and outputs.
$ nix develop$ python3 --versionPython 3.13.15
Now back to the two laptops. Copy the directory to both, flake.lock included,
and step into the shell. On the one with this year’s nixpkgs:
$ nix develop$ python3 --versionPython 3.13.15
And on the one with last year’s:
$ nix develop$ python3 --versionPython 3.13.15
Same flake, same lock, same Python. The nixpkgs each laptop has doesn’t matter
anymore. That’s the whole point of flakes.
Most flakes live in a Git , and that’s where almost everyone
trips up once. Nix only sees the files Git knows about. Say you’ve run
gitinit in the directory, but haven’t added anything yet. Step into the
shell, and Nix acts like the flake isn’t there:
$ nix developerror: Path 'flake.nix' in the repository "/home/yurii/my-flake" is not tracked by Git. To make it visible to Nix, run: git -C "/home/yurii/my-flake" add "flake.nix"
The error even tells you the fix. Add the files, and it works:
$ git add .$ nix developwarning: Git tree '/home/yurii/my-flake' is dirty$ python3 --versionPython 3.13.15
You don’t even have to commit. The warning only means you have changes you
haven’t committed yet. Commit when you’re happy, and it goes away.
The lock never moves by itself. When you do want newer packages, run
nixflakeupdate. It moves every input to the newest commit on its branch,
and writes that into the lock:
Your flake.nix didn’t change. It still asks for nixos-26.05. Only the lock
caught up with the branch.
Don’t like what the update brought? The old lock is still in Git, so put it
back, and you’re on 774debe again:
$ git restore flake.lock
On with a flake, this is how you update your whole system. Run
nixflakeupdate, then . And if something breaks, restore the old
lock and rebuild again.
Outputs aren’t only dev shells. Each kind has its own name, and the command
that uses it knows where to look:
devShells are shells, for nixdevelop.
packages are programs, for nixrun and nixbuild.
nixosConfigurations are whole NixOS systems, for nixos-rebuildswitch--flake.
homeConfigurations are setups, for home-managerswitch--flake.
Most flakes you’ll find look a little different from ours:
flake.nixFlakeA flake with inputs and outputs.
It’s the same flake, written shorter in two places. First, the inputs are
unpacked right where outputs starts, so it can say nixpkgs instead of
inputs.nixpkgs. self is the flake itself, and the ... lets outputs
ignore any inputs it doesn’t name.
Second, nixpkgs.legacyPackages.x86_64-linux takes the place of the whole
import. It’s the same package set, already imported by nixpkgs for every kind
of machine. You’ll see this shorthand everywhere. The name doesn’t mean the
packages are old.
The flake playground lets you wire all of this up yourself. Pick inputs, plug
outputs into them and commands into the outputs, and watch the flake.nix it
writes for you. Try to break it, too:
Toolbox
Add inputs, outputs and commands, and watch flake.nix change.
Inputs
Outputs
CLI
flake.nix
flake.lock
No inputs locked yet
reviewed lock states
Flake playground
Try it
Get the newest Python. Change nixos-26.05 in the url to nixos-unstable.
Point nixpkgs at the nixos-unstable branch
flake.nixFlakeA flake with inputs and outputs.
The lock still holds a nixos-26.05 commit, so the next time you use the
flake, Nix moves that input over on its own:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
A is an address Nix knows how to fetch a from. You type
them all the time:
nixrunnixpkgs#hello
nixbuild.#hello
sudonixos-rebuildswitch--flake.#snowflake
The part before # is the address. The part after it is what you want from
that flake.
. is the flake in the directory you’re in.
nixpkgs is a short name. Nix looks it up in its , a of names and
the addresses they stand for. Out of the box, nixpkgs means
nixpkgs-unstable. On built from a , commands like
nixrunnixpkgs#hello get the your system is built from instead.
You also write one in a flake’s , with the same syntax. It’s the
in url, like inputs.nixpkgs.url="github:NixOS/nixpkgs/nixos-26.05";.
There, nixpkgs still means nixpkgs-unstable, even on NixOS.
Every address is made of a few parts: how to fetch it, where it lives, and
sometimes which version. Click the parts of each address to see what they do:
$nixbuild
short nameNix looks the name up in its registry. There, nixpkgs points at the latest nixpkgs-unstable.
What gets pinned: Handy for quick commands. In a flake, flake.lock writes down the real address and the exact commit it found.
nix build nixpkgs#hello
The part after the picks the version. It can be a branch, a tag or
an exact commit:
github:NixOS/nixpkgs/nixos-26.05 is a branch. It moves forward as the
branch gets updates. In an input, flake.lock still keeps you on one
until you run nixflakeupdate.
github:NixOS/nixpkgs/26.05 is a tag. It names one commit and stays there.
github:NixOS/nixpkgs/2efa67fd26b6df417c33e4603185c701f260dd83 is one exact
commit.
And every address is also a shorthand for an , where each of those
pieces gets a name. These two nixpkgs inputs are the same input:
flake.nixFlakeA flake with inputs and outputs.
flake.nixFlakeA flake with inputs and outputs.
The short form is the one you’ll see almost everywhere. The long form is handy
when an address has many parts. Take
git+https://example.org/team/tool?ref=v2&dir=nix. It fetches a repository
from its v2 branch. Then it uses the flake in the nix directory. In the long
form, each part gets its own line:
flake.nixexcerptNixA plain Nix expression.
Try it
Write the home-manager input in its long form. Replace its url line with inputs.home-manager = { };, and give it one line each for type, owner, repo and ref.
Fetch nix-community/home-manager from GitHub
Follow the release-26.05 branch
flake.nixFlakeA flake with inputs and outputs.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Most nix commands take an address like nixpkgs#hello. You
type one every time you run a program or try it out in a :
$ nix run nixpkgs#cowsay -- hi ____< hi > ---- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || ||$ nix shell nixpkgs#hello$ helloHello, world!
Different commands, same kind of address. Nix calls it an : a
, a #, and the path of an inside that ’s
.
The part before # is an ordinary flake reference. nixpkgs is a nickname
Nix looks up in its flake , a of short names for well-known
flakes. Out of the box it stands for nixpkgs-unstable, so that branch’s address on
gets you the same :
The part after # is the path to the thing you want. In , hello sits
deep in the outputs, under legacyPackages, then your system, then its name.
You can type all of that:
But you almost never do, because the short name finds the same package:
$ nix eval nixpkgs#hello.version"2.12.3"
Nix fills in the start for you, for the system you’re on. The
system is never in the short name. Meaning the same nixpkgs#hello works on a
Linux PC and on a Mac, and each one gets the hello built for it.
Misspell a name, and the error shows every place it looked:
$ nix eval nixpkgs#heloerror: flake 'flake:nixpkgs' does not provide attribute 'packages.x86_64-linux.helo', 'legacyPackages.x86_64-linux.helo' or 'helo' Did you mean one of halo, hello, helm, below or belr?
Each command looks in its own place first. nixrun tries apps, and
nixdevelop tries devShells:
$ nix run nixpkgs#heloerror: flake 'flake:nixpkgs' does not provide attribute 'apps.x86_64-linux.helo', 'packages.x86_64-linux.helo', 'legacyPackages.x86_64-linux.helo' or 'helo' Did you mean one of halo, hello, helm, below or belr?$ nix develop nixpkgs#heloerror: flake 'flake:nixpkgs' does not provide attribute 'devShells.x86_64-linux.helo', 'packages.x86_64-linux.helo', 'legacyPackages.x86_64-linux.helo' or 'helo' Did you mean one of halo, hello, helm, below or belr?
Last of all, Nix tries the path exactly as you wrote it. That’s how you reach
anything else in a flake. nixpkgs has a output next to legacyPackages,
so Nix finds it at the exact path you typed:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
have inputs of their own. ’s depends on too,
and Nix locks that one to whatever commit Home Manager’s own flake.lock
says. So with just two inputs in your flake, you already get two copies of
nixpkgs:
The second copy is gone. But Home Manager now runs on a
nixpkgs commit its authors didn’t lock themselves. That’s why you pair
matching releases, release-26.05 with nixos-26.05, so the two stay close.
You’ll write a line like this for almost every input that brings its own
nixpkgs, like Home Manager, or sops-nix.
Try it
Under the home-manager.url line, add home-manager.inputs.nixpkgs.follows = "nixpkgs";.
Make Home Manager's nixpkgs follow yours
flake.nixFlakeA flake with inputs and outputs.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
A NixOS takes every from one , usually a release
like nixos-26.05. Sometimes you want one program newer than your
branch has, without moving the whole system to nixos-unstable. For that, add
a second nixpkgs on the unstable branch, and take only that program from
it:
The new input gives you its packages under legacyPackages.x86_64-linux, the
for 64-bit Intel and AMD Linux. It’s the same kind of thing
is, only from the unstable branch.
is a set of extra every can ask for. Here it
hands over that set as pkgs-unstable.
So yurii gets from nixos-26.05 and Zed 1.21.0 from nixos-unstable,
while the system’s own pkgs still has Zed 1.3.6:
nixpkgs still refuses VS Code. And among its suggestions is the very line you
already have:
$ nix eval .#nixosConfigurations.snowflake.config.users.users.yurii.packages[ «derivation /nix/store/0x5gbp616f0hdkpm4hm9cdc48sk30ygn-git-2.54.0.drv» «error: Refusing to evaluate package 'vscode-1.140.0' in /nix/store/rhsxvv1380jmchb5p3a43d24mhlmp2ry-source/pkgs/applications/editors/vscode/vscode.nix:104 because it has an unfree license (‘unfree’)a) To temporarily allow unfree packages, you can use an environment variable for a single invocation of the nix tools.…b) For `nixos-rebuild` you can set { nixpkgs.config.allowUnfree = true; }in configuration.nix to override this.…
That’s because nixpkgs.config only shapes pkgs. Your pkgs-unstable never
sees it.
nixpkgs itself is a . Import the input, and you can call it with
settings of its own:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
A ’s packages and devShells need one entry per system. Say you want
hello as the default on Linux PCs, ARM Linux machines and Apple
Silicon Macs. A plain flake writes the same line three times, once under each
, and asks for the matching legacyPackages each time:
flake.nixFlakeA flake with inputs and outputs.
Now offer as a second package. That’s three more lines, one per system
again:
flake.nixFlakeA flake with inputs and outputs.
Every package gets copied once per system, and so does every .
flake-parts is a that writes that part for you. It isn’t part of Nix or nixpkgs. It
lives in the hercules-ci/flake-parts on , and you add it
like any other .
You hand flake-parts.lib.mkFlake your inputs and a set. systems lists the
systems you support. perSystem is written once, as a that gets the
right for each of them. Here’s the same flake again, with one line per
package:
flake.nixFlakeA flake with inputs and outputs.
The inputs@ gives the whole set of inputs one name, inputs, because
mkFlake wants all of them.
Both flakes give the same . nixflakeshow lists the same six
packages for either one:
You've edited flake.nix, but this output came from the original.
There’s no system name inside perSystem. flake-parts takes
pkgs from your nixpkgs input, already picked for the system, and files
each package under every system in the . That’s also why it wants an input
called nixpkgs.
So supporting another system means adding one word to systems, not one more
line for every package.
Not every output belongs to a system. A machine already says what it
runs on in its own , so nixosConfigurations has no system names
in it. Outputs like that go under flake, next to perSystem, and come out
exactly as you write them:
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
You've edited flake.nix, but this output came from the original.
snowflake sits right under nixosConfigurations, with no system name around
it. The package still got one per system.
The set you hand mkFlake is a , the same
kind as a NixOS configuration. So it can have imports, and a big flake.nix
splits into a few smaller files. Here perSystem moves into its own file. It
also gets a dev shell, which is copied per system just like a package:
packages.nixProject filesA file in the example project.
flake.nixFlakeA flake with inputs and outputs.Project filesA file in the example project.
You've edited flake.nix packages.nix, but this output came from the original.
$ nix flake showgit+file:///tmp/tmp.PcQeByc5aH/my-tool?ref=refs/heads/main&rev=2d2c35f652e0b6a8a31d74214f13e63cd15d82c0├───devShells│ ├───aarch64-darwin│ │ └───default omitted (use '--all-systems' to show)│ ├───aarch64-linux│ │ └───default omitted (use '--all-systems' to show)│ └───x86_64-linux│ └───default: development environment 'nix-shell'└───packages ├───aarch64-darwin │ ├───cowsay omitted (use '--all-systems' to show) │ └───default omitted (use '--all-systems' to show) ├───aarch64-linux │ ├───cowsay omitted (use '--all-systems' to show) │ └───default omitted (use '--all-systems' to show) └───x86_64-linux ├───cowsay: package 'cowsay-3.8.4' └───default: package 'hello-2.12.3'
The same six packages are there, and the dev shell came out once per system.
Say you want the home-manager command in your dev shell. It comes from
another flake, so you add it as a home-manager input. That flake files its
packages under system names too.
Every file you hand mkFlake can ask for your inputs at the top. Inside perSystem,
system is the name of the current system. With those two you can reach in
the long way:
packages.nixProject filesA file in the example project.
perSystem also gets inputs', with a quote at the end. With it, the same
line gets shorter:
packages.nixProject filesA file in the example project.
They’re the same package. inputs' already picked the system for you.
You’ll meet this a lot, because many flakes on GitHub are written with
flake-parts. Here’s how nixvim’s flake.nix ends:
It’s the same mkFlake, with systems and imports. nixvim takes inputs
as one name instead of inputs@{...}, and reaches flake-parts through it
with a dot. Both ways hand mkFlake every input.
Its systems comes from an input too. That input is a tiny repository with
nothing but a list of system names. The rest of the flake lives in its flake
directory, pulled in by imports.
Try it
Support ARM Linux machines too. Add "aarch64-linux" to the end of systems.
Offer hello on ARM Linux machines
flake.nixFlakeA flake with inputs and outputs.
Offer cowsay on every system too. Add packages.cowsay = pkgs.cowsay; under the packages.default line.
Offer cowsay on Linux PCs and on Apple Silicon Macs
flake.nixFlakeA flake with inputs and outputs.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
A program built for a Linux PC won’t run on an Apple Silicon Mac. That’s why
packages and devShells have an extra level with a in it, like
packages.x86_64-linux.default or packages.aarch64-darwin.default.
A system name is just a CPU architecture and an operating system joined with a
dash. You’ll mostly meet four: x86_64-linux for ordinary Linux PCs,
aarch64-linux for ARM Linux machines, aarch64-darwin for Apple Silicon
Macs and x86_64-darwin for Intel Macs.
flake.nixFlakeA flake with inputs and outputs.
Note how each line names its system twice. Once to say who the is
for, and once to ask for a hello built for that system, because
legacyPackages has a separate for every system it supports.
The name after packages only says where the package is filed, the one after
legacyPackages decides what gets built. Mix them up and Nix won’t complain,
it just hands a Mac a package built for Linux.
You rarely type the system on the command line. Nix fills in the one you’re
running on, so on a Linux PC nixbuild.#cowsay looks for
packages.x86_64-linux.cowsay, and nixflakeshow doesn’t even
the systems that aren’t yours:
$ nix flake showgit+file:///tmp/my-tool?ref=refs/heads/master&rev=2bf86330b3d2bbba070f86cdd4dcd72eeb8c4110└───packages ├───aarch64-darwin │ └───default omitted (use '--all-systems' to show) └───x86_64-linux └───default: package 'hello-2.12.3'$ nix build .#cowsayerror: flake 'git+file:///tmp/my-tool' does not provide attribute 'packages.x86_64-linux.cowsay', 'legacyPackages.x86_64-linux.cowsay' or 'cowsay'
nixosConfigurations don’t get this level, because a NixOS
names its system inside.
Try it
Support ARM Linux machines too. Copy the x86_64-linux line, paste it right below, and change both x86_64-linux in the copy to aarch64-linux.
Offer packages.aarch64-linux.default
Build it for ARM Linux
flake.nixFlakeA flake with inputs and outputs.
Someone copied the Linux line for Macs and changed only the first system. Change legacyPackages.x86_64-linux in the aarch64-darwin line to legacyPackages.aarch64-darwin.
Give Macs a hello built for aarch64-darwin
flake.nixFlakeA flake with inputs and outputs.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
When your lives in a Git , Nix only lets it see files
knows about. A file you just created and never added is invisible, even though
it sits right next to flake.nix.
That’s because Nix copies your flake into the
before it reads it, and it asks Git which files
to copy. Meaning files Git doesn’t track never end up in the store. That keeps
out a result link you listed in .gitignore. But it also keeps out a file you
forgot to add.
Here flake.nix reads a second file, shell.nix, with . But
shell.nix was never added to Git:
$ git status --short?? shell.nix$ nix developerror: … while calling the 'import' builtin at /tmp/my-project/flake.nix:5:38: 4| outputs = { self, nixpkgs }: { 5| devShells.x86_64-linux.default = import ./shell.nix { | ^ 6| pkgs = nixpkgs.legacyPackages.x86_64-linux; error: Path 'shell.nix' in the repository "/tmp/my-project" is not tracked by Git. To make it visible to Nix, run: git -C "/tmp/my-project" add "shell.nix"$ git add shell.nix$ nix develop --command node --versionwarning: Git tree '/tmp/my-project' is dirtyv24.21.0
Nix even prints the command that fixes it. Note that gitadd is enough,
because Nix needs the file tracked, not committed. Until you commit, it only
warns that the Git tree is dirty and carries on.
So the rule of thumb: every time you create a new file for your flake,
gitadd it before you run anything.
Put your configuration on , so you can get it back on any machine.
Takes about 35 minutes.
You’ll:
turn ~/nixos-config into a repository, and see why Nix wants your files
added to Git
make your first commit
give GitHub an SSH key and push your configuration to it
Every project needs its own tools. A app wants node and npm, a
script wants Python. Install all of them system-wide and your machine slowly
fills up with tools for projects you finished long ago, while a teammate still
has to guess which ones you used. pkgs.mkShell lets the project write its
tools down in Nix instead.
It’s a from that takes one . packages is a
of whose commands end up on your
inside the . shellHook is shell code that
runs every time you enter. And any mkShell doesn’t know, like
PORT here, becomes an
variable with that .
Meaning inside this shell node, npm and python3 just work, $PORT is
3000, and Node.js prints its version as you enter. Run nix-shell in the
directory with this file to step inside, and exit to leave. A puts the
same mkShell call in its devShells instead.
You've edited shell.nix, but this output came from the original.
You don’t have to step inside a shell to see what it sets. A shell is a
too, so you can import shell.nix in
the and read its attributes like any other value:
shell.nixread only
The first value is "nix-shell". That’s the name mkShell picks when you
don’t give one.
And PORT is just an attribute on it, a plain string. Entering the
shell is what turns it into an environment variable.
Plenty of shell.nix files on GitHub write nativeBuildInputs where this
one writes packages. That’s the older way, and it gives the very same shell:
So mkShell moves packages into nativeBuildInputs for you.
Say you want to work on itself. You’d need every tool its build needs,
and you’d rather not look them up. inputsFrom does that for you. Hand it
packages, and the shell gets everything they need to build. cowsay is written
in Perl, so its asks for perl, and perl shows up in the shell:
shell.nixNIX_PATHThe Nix search path supplies `<nixpkgs>`.Project filesA file in the example project.
You've edited shell.nix, but this output came from the original.
$ nix-shell[nix-shell:~/my-project]$ which perl/nix/store/myzwgd3ylpl3d8zrl7kl3l97i7zl8mw6-perl-5.42.0/bin/perl[nix-shell:~/my-project]$ exitexit
Try it
Here pkgs is already loaded for you, and the last line lists the names in nativeBuildInputs. Your app needs Python too. Add pkgs.python3 after pkgs.nodejs in packages.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
A project often needs its own tools, like a certain version of or
Rust. A can describe a that has them, in
its devShells . nixdevelop is the command that opens it.
Meaning you don’t install anything to work on a project like that. You run
nixdevelop, and the tools are there. nh, a helper for rebuilding , is
written in Rust. Its flake has a shell with cargo and the rest of its tools.
You can even open it straight from :
Every teammate gets this exact cargo. The project’s
pins the it comes from.
Your own shell is just a few lines:
flake.nixFlakeA flake with inputs and outputs.
Each shell is a pkgs.mkShell call, and packages lists what goes in it.
comes from the flake’s nixpkgs .
In this example, the flake offers one shell with Node.js. Look at the path
devShells.x86_64-linux.default. First comes the system the shell is for,
here ordinary 64-bit Linux PCs. Then comes the shell’s name, default.
x86_64-linux appears twice, once in
legacyPackages.x86_64-linux and once in devShells.x86_64-linux. The
shell’s have to be built for the same kind of machine the shell is
for. So a teammate on an Apple Silicon Mac finds no shell here until someone adds one under aarch64-darwin.
nixflakeshow lists what a flake offers. This one has a single shell,
default, for x86_64-linux:
You've edited flake.nix, but this output came from the original.
$ nix flake showwarning: Git tree '/tmp/my-app' is dirtywarning: creating lock file "/tmp/my-app/flake.lock": • Added input 'nixpkgs': 'github:NixOS/nixpkgs/774debe7a0d1b496e35677ad955a1011c6ff74f3?narHash=sha256-nQyFkMR78WP6PiXKkDW2SjqzLf/spQqjRuxRGSbcV8k%3D' (2026-10-02)git+file:///tmp/my-app└───devShells └───x86_64-linux └───default: development environment 'nix-shell'
Run nixdevelop in the flake’s directory, and you’re in that shell:
You've edited flake.nix, but this output came from the original.
$ nix developwarning: Git tree '/tmp/my-app' is dirty$ node --versionv24.21.0
The name default is special. It’s the shell nixdevelop picks when you
don’t ask for one by name. A project can offer more next to it, say a second
shell with only the tools its Python scripts need.
Try it
Copy the three lines of the default shell and paste them right below it. In the copy, change default to scripts and pkgs.nodejs to pkgs.python3.
Offer a shell named scripts
Put pkgs.python3 in it
flake.nixFlakeA flake with inputs and outputs.
Now nixflakeshow lists both shells:
You've edited flake.nix, but this output came from the original.
$ nix flake showwarning: Git tree '/tmp/my-app' is dirtygit+file:///tmp/my-app└───devShells └───x86_64-linux ├───default: development environment 'nix-shell' └───scripts: development environment 'nix-shell'
Plain nixdevelop still gives you Node.js. To pick the other one, add
.#scripts. The . means this directory, and after # comes the shell’s name:
You've edited flake.nix, but this output came from the original.
$ nix develop .#scriptswarning: Git tree '/tmp/my-app' is dirty$ python3 --versionPython 3.13.15
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
nixdevelop puts you in a project’s : a Bash with the
tools the project needs on your , without
installing any of them system-wide. The project describes that shell in the
devShells of its flake.nix, like this one:
flake.nixFlakeA flake with inputs and outputs.
Here, the default shell asks for , sets PORT to 3000, and runs
node--version as its shellHook when you enter. The scripts shell only
has Python.
Plenty of projects on ship a devShells output like this. You don’t
even have to clone them. Here’s the shell of nh, a helper for NixOS ,
straight from GitHub:
The version shows up twice, because shellHook runs before your
command too.
When a package fails to build, it helps to run the build one step at a time.
nixdevelop gives you that for any package: the shell its build runs in, with
its and , where each build step is a command:
$ nix develop nixpkgs#hello$ unpackPhaseunpacking source archive /nix/store/wj7phsmi7ncidl8k00p489krqss7n9sd-hello-2.12.3.tar.gzsource root is hello-2.12.3setting SOURCE_DATE_EPOCH to timestamp 1773804383 of file "hello-2.12.3/ChangeLog"$ cd hello-2.12.3$ configurePhase…$ buildPhase…$ ./helloHello, world!
Between any two steps, you can look at the files and see where it goes wrong.
This page needs flakes and the nix command, both still experimental. Turn them on first.
On NixOS, for every user. Add this to configuration.nix, and rebuild:
Just this once, with nothing saved. Put the flag right after nix:
$ nix --extra-experimental-features "nix-command flakes" run nixpkgs#helloHello, world!
Every project has its own tools. You don’t want to type a command to get them
each time you open a there. direnv loads them for you when you cd
into the directory.
It hooks into your . Whenever you enter a directory
with an .envrc file, it loads that directory’s into the shell you
already have. Leave the directory, and it unloads it again.
Editors with a direnv plugin, like VS Code and Neovim, load the same
environment. So your language server and formatter come from the project too.
Say a project’s offers a shell with :
flake.nixFlakeA flake with inputs and outputs.
For a flake project, the whole .envrc is one line:
$ cat .envrcuse flake
useflake loads the flake’s devShells.x86_64-linux.default, the shell above
with Node.js in it. It also watches flake.nix and flake.lock. Edit one of
them, and the shell reloads on your next command.
On its own, useflake the flake again every time you enter the
directory. nix-direnv fixes that. It caches the shell and only evaluates the
flake again after you change it.
The easy way to get both is your . On , one line installs
direnv, hooks it into your shells and turns nix-direnv on as well:
leaves nix-direnv off by default, so there you turn on both:
home.nixexcerptNixA plain Nix expression.
Anywhere else, you add the hook yourself. For , that’s the line
eval"$(direnv hook bash)" at the end of ~/.bashrc. direnvhookzsh and
direnvhookfish print the versions for those shells. Here it runs by hand:
You've edited flake.nix, but this output came from the original.
The long export line lists every variable the shell set. Only the ~PATH
near the end matters here. The ~ means your changed.
That’s the whole trick. node is there inside the directory and gone outside
it.
Notice how direnv refused the file at first. An .envrc can run any command,
and it could come from a you just cloned. So you approve it once
with direnvallow, and again after every change to it.
A mkShell gives you the tools, but setting them up is on you. And that often
means knowing how your language works inside.
Take Python. Put pkgs.python3 in a mkShell, and you can’t even install a
library:
$ nix develop$ pip install requestsbash: pip: command not found$ python3 -m pip install requests/nix/store/9r02ykx9y35lf4gk6gc30gnag1y8ncp7-python3-3.13.15/bin/python3: No module named pip
To fix it, you need to know how Python finds its libraries. Either you list
them in Nix with python3.withPackages, or you make a virtual environment, a
folder in your project that pip installs into. That one also needs a
shellHook that creates it the first time and turns it on every time you
enter.
Every language has its own quirks like that. knows them for you. Its
Python support alone is about 950 lines of Nix, and it covers over 50
languages. You turn a language on in a file called devenv.nix:
devenv.nixdevenvA devenv module.
devenvinit starts you off. It writes a devenv.nix full of examples, plus
devenv.yaml and a .gitignore. Put the lines above in devenv.nix, and
devenvshell takes you inside:
You've edited devenv.nix, but this output came from the original.
The devenv:python:virtualenv line is devenv making the virtual environment
for you, in .devenv/state/venv.
devenv.yaml lists the , like the inputs of a . The first
devenvshell also writes devenv.lock, which pins them like a
. Commit devenv.nix, devenv.yaml and devenv.lock, and your
teammates get the same shell.
devenv can also run what your project talks to, like a database. PostgreSQL is
one more line:
devenv.nixdevenvA devenv module.
devenvup starts it and shows how it’s doing. Press Ctrl+C twice to stop it:
You've edited devenv.nix, but this output came from the original.
$ devenv up…✓ Running processes 12.1s … └ ● postgres ready 43.6s … 2026-10-13 18:45:06.164 GMT [3012844] LOG: database system is ready to accept connections Exec probe succeeded - process ready
The database lives in .devenv/state/postgres, inside your project. So every
project keeps its own data.
Leave it running and open the shell in a second . psql is there,
and it already knows where the database is:
You've edited devenv.nix, but this output came from the original.