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

Install Nix1.

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 the url above, after unpacking the gzipped tarball. Don’t worry if any or all of that was gobbledygook. You can run nix-prefetch-url --unpack URL, replacing URL with the url 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!

  1. Really, that’s it. 

  2. The output might be different for you. This just happens to be what the version installed on my system prints.