Nix shell template
Nix shells are the best tool for creating software development environments right now. This article provides a template to get you started with Nix shells from scratch, and explains how to add common features.
The template has the following features:
- Everybody using the template gets the same package versions, because it locks in the version of the entire package collection in a single expression. This also means there’s no need for a separate lock file.
- Seamlessly integrates with software already installed on your platform, with a single flag to make sure only the Nix shell packages are available for improved reproducibility.
- Does not interfere with software already installed on your platform. All the Nix packages are installed in their own directory.
- Add a package with a single line change.
- Uses a subset of the packaging-specific, declarative, lazily evaluated, dynamically typed, purely functional Nix language.
Prerequisites
The template
This is basically the simplest reproducible Nix shell declaration you can get:
let
pkgs = import (fetchTarball {
name = "nixos-23.05_2023-06-30";
url = "https://github.com/NixOS/nixpkgs/archive/b72aa95f7f096382bff3aea5f8fde645bca07422.tar.gz";
sha256 = "1ndnsfzff0jdxvjnjnrdm74x8xq2c221hfr7swdnxm7pkmi5w9q5";
}) { };
in
pkgs.mkShell { packages = [ pkgs.bashInteractive ]; }
This should be stored in a shell.nix
file in your project root.
Use
Enter the shell by running nix-shell
in the same directory as the shell.nix
file above. This will download the nixpkgs tarball and the Bash package, and
cache both of them for (much faster) future runs.
Nix shells do not automatically reload when you change shell.nix
. To load an
updated shell.nix
, exit the current Nix shell (if you’re already in it), then
re-run nix-shell
.
Adding packages
To install another package in your Nix shell, add it to the packages
list.
(List entries are whitespace separated, so there’s no need for any commas.) For
example, to install the default Python 3 interpreter, add a line below
pkgs.bashInteractive
with pkgs.python3
.
You can search through supported packages or older packages. (Mixing packages from different nixpkgs versions in the same Nix shell is beyond the scope of this article.)
Pure Nix shells
This might come as a surprise2:
$ nix-shell --run 'git --version'
git version 2.40.1
Git is not installed in the Nix shell, but it still runs successfully. It turns out that this is the Git version installed in the underlying OS. This is a feature during normal development: you probably don’t want your IDE and other auxiliary tools to bloat your Nix shell, since they shouldn’t affect the project outputs. But this decreases reproducibility. In the best case, another developer or your automated build complains that the command is not available. Worse, they may get subtly different results, resulting in lost time working out why. At worst you don’t find out until customers complain about a broken product.
The fix: run nix-shell --pure
to avoid inheriting variables such as PATH
from the underlying shell:
$ nix-shell --pure --run 'git --version'
[omitted] git: command not found
Basically, make sure to use only pure Nix shells in your automated builds (and run automated builds when pushing to unmerged branches). This way you get the best of both worlds: builds are reproducible, but everyone can use whichever auxiliary tools they want during development.
nixpkgs version updates
The fetchTarball
function call in the template specifies the exact version of
nixpkgs to use for all the packages in the Nix shell. So to update packages, you
will need to update the function call:
name
is basically an arbitrary identifier, and I chose to use the format[nixpkgs branch name]_[date of commit in URL]
. You can of course choose whatever you like, but including these two makes it pretty easy to check whether the packages are recent or not.url
is where you can download the tarball for a specific version of nixpkgs. You could point it to any URL, but you probably want to use one of the commit IDs listed on the nixpkgs status page. I chose to use the commit at the head of the most recent release branch, “nixos-23.05”, which is effectively the “latest stable” nixpkgs right now.sha256
is the SHA-256 checksum of the contents of theurl
above, after unpacking the gzipped tarball. Don’t worry if any or all of that was gobbledygook. You can runnix-prefetch-url --unpack URL
, replacingURL
with theurl
you specified, and replace the value with the last line that command prints.
Of course, the above is a bit clunky, especially if you want to update often. In
that case I would recommend using Niv. You
can see how that works in the
blog repository -
the shell.nix
imports a separate static Nix file, which reads configuration
from a dynamic JSON file. Both files are maintained by the niv
command and
versioned like any other file in your repository.
Garbage collection
You will probably end up changing the nixpkgs URL several times over the life of
a project. As versions change, more packages are saved locally. To avoid wasting
space, it is a good idea to run nix-collect-garbage
once in a while to delete
unreferenced Nix store paths.
Summary
At this point you’re right to suspect that I’ve glossed over many details. Why
bashInteractive
? How can I avoid ever having to wait for packages to build?
What about cross-platform compatibility? What about supporting my colleagues who
can’t/won’t use Nix? How do I spin up a PostgreSQL server with pgTAP in a
single
command?
How do I combine Nix shells for crazy internet points? Are there still cases
where this setup can be non-reproducible? I only know the answer to some of
those, and if you’re interested in more I’ll try to answer them.
This is all you need to know to use Nix shells productively for many projects. And once you need something more advanced, rest assured it can be done without losing any of the advantages listed above.
Acknowledgements
Thanks to Adam Höse and Ivan Minčík for reviewing a draft of this post!