Nix shells - tips, tricks, and best practices

WARNING
None of this is authoritative! Please take everything with a grain of salt, and as this is a wiki post, please consider editing it. The whole point of this post is to initiate a discourse and to improve the official documentation; see the diagram in How to contribute to documentation.
Thank you!

    1. About Nix shells
    • 0.1 What are Nix shells?
    • 0.2 Use cases (and sources of confusion)
      • 0.2.1 Develop Nix packages
      • 0.2.2 Create ad hoc environments
      • 0.2.3 Examples
        • 0.2.3.1 nix-shell -p
        • 0.2.3.2 nix-shell with a default.nix
    • 0.3 Benefits of using Nix shells
    • 0.4 How to start applications from the terminal that won’t close with the terminal?
    1. One-liners with pinning
    • 1.1 Get a single package with nix-shell -p + callPackage
    • 1.2 Improvise a shell.nix on the terminal
      • 1.2.1 mkShell vs mkDerivation
      • 1.2.2 Advantages of mkShell
    • 1.3 Nix shell using multiple packages with specific versions (i.e., multiple Nixpkgs revisions)
    1. Using nix-shell with shell.nix
  • 2.x Examples

TODO: Still a draft; have about 40 tabs open with nix-shell related stuff.

0. About Nix shells

0.1 What are Nix shells?

nix-shell and nix develop start an interactive Bash shell, and, depending on their arguments, they will

  • pre-populate the subshell with stuff (e.g., environment variables are set, applications will be available in PATH)

  • start and configure services (e.g., launch a PostgreSQL instance and set it up)

  • ?

QUESTION: Does nix run belong here? What else is missing?

TODO: Weave the Development environment with nix-shell nixos.wiki article into this - or vice versa.

0.2 Use cases (and sources of confusion)

0.2.1 Develop Nix packages

Instead of building (or realizing) the package (e.g., software, book, data set) with nix-build (or siblings), one will get dropped into a Nix shell with all the dependencies and requirements (e.g., environment) of the package right before it would be built / realized.

That is, if the packages build instructions only ask for make to be executed, then if you issue make on this shell, then the package will be built, and a link will be provided to the result.

0.2.2 Create ad hoc environments

Examples:

  • developing an application, so you need a couple of scripting language runtimes with packages, debugging tools, a database instance running in the background, etc.

  • ops work on local/remote VMs (or one just has 12 old ThinkPads lying around) with different operating systems, so firing up a Nix shell with the tools one is used to (of specific versions!)

  • ?

0.2.3 Examples

0.2.3.1 nix-shell -p
$ nix-shell -p peek treesheets yt-dlp

will drop you in a shell where the applications peek, treesheets, and yt-dlp are readily available.

0.2.3.2 nix-shell with a default.nix

TODO: There should be a better example (perhaps a small example repo), but couldn’t come up with anything better this quickly… (Or, at least, add (or redirect to places with) info about -E and callPackage.

Let’s take the treesheets package for example:

$ git clone https://github.com/NixOS/nixpkgs.git
$ cd nixpkgs/pkgs/applications/office/treesheets/
$ nix-shell -E 'with import <nixpkgs> { }; callPackage ./default.nix { }'
$ <the build commands themselves, e.g., cmake>

If you try to execute treesheets in this shell, you’ll get

treesheets: command not found

0.3 Benefits of using Nix shells

  • Make NixOS / Home Manager configuration lighter by only putting installables there that require extra config.

    QUESTION: Although, these could be Nix shellified too, right?

  • Avoid depending on channels (and thus improving reproducibility) by evaluating the Nixpkgs package collection fetched at a specific commit (or the same with an alternate or one’s own package collection).

    QUESTION: Flakes do something similar, don’t they?..

  • Create a sub-shell with tools for a specific project (e.g., PostgreSQL with config, web framework with all its dependencies)

  • To work on machines / VMs that are temporary, rarely used, reset periodically, etc., so just install Nix and use nix-shell with the most common tools with the required versions.

  • ?’

0.4 How to start applications from the terminal that won’t close with the terminal?

$ nohup google-chrome-stable & > ~/.nohup.out # or /dev/null
$ disown

See this great answer on nohup vs disown.

1. One-liners with pinning

NOTE: Using the google-chrome package to also demonstrate dealing with unfree packages. See 2.3. Installing unfree packages in the Nixpkgs manual.

ASIDE: tidbits on builtins.fetch* functions

1.1 Get a single package with nix-shell -p + callPackage

TODO: Add aside about callPackage.

TODO: The Nix reference manual on nix-shell’s -p / --packages doesn’t mention this behaviour and I learned this from @abathur.
update: It is documented in the nix-shell “Examples” section.

NIXPKGS_ALLOW_UNFREE=1                                                                             \
nix-shell -v -p                                                                                    \
'(callPackage                                                                                      \
   (fetchTarball https://github.com/NixOS/nixpkgs/tarball/dc849ffbcd93c2a23e99dcc94efb0962594b8b5f \
   ) {}                                                                                            \
 ).google-chrome'

1.2 Improvise a shell.nix on the terminal

NOTE: I believe -E or --expr is only documented in 7.1. Common Options in the Nix manual (or, at least) the only place I saw it in the man pages was in man nix-instantiate, but even there it is just mentioned.)

An ad hoc development sub-shell:

NIXPKGS_ALLOW_UNFREE=1                                             \
nix-shell -v -E                                                    \
"let                                                               \
   pkgs = import (fetchTarball ${NIXPKGS_TARBALL}) {};             \
 in                                                                \
   pkgs.mkShell {                                                  \
     buildInputs = with pkgs; [ google-chrome elixir postgresql ]; \
   }"

1.2.1 mkShell vs mkDerivation

Older (< 2018) scripts tend to use mkDerivation instead of mkShell, but the latter is only a convenience wrapper around the former when writing Nix expressions for nix-shell. See mkShell needs more documentation in the manual #58624 Nixpkgs issue for more info and links.

1.2.2 Advantages of mkShell

(For creating dev environments, that is.)

  • accepts an inputsFrom list, to “to pull all the dependencies together” from a large project, for example. From the initial PR for mkShell:

    […] you have multiple projects each with their own shell.nix , and you could create 1 master shell.nix that merges all subproject’s shell.nix in case you wanted to work on all the subprojects together. (CMCDragonkai’s comment)

    Imagine you have a monorepo with multiple services, each in their own folder with their own default.nix that you can nix-shell and nix-build. On the top-level you can then add a shell.nix with, mkShell { inputsFrom = [ serviceA serviceB ]; to pull all the dependencies together. (zimbatm’s comment)

  • shellHook s are also composed form input expressions. See PR #63701 for examples.

Tons of good info in this Elixir Discourse thread.

1.3 Nix shell using multiple packages with specific versions (i.e., multiple Nixpkgs revisions)

This comes from this SO answer:

nix-shell -E '
let
  pkgsA = (import (builtins.fetchTarball https://github.com/NixOS/nixpkgs/archive/dfd7183bb1077c064c15575f1dc37a754e13bfc9.tar.gz) {});
  pkgsB = (import (builtins.fetchTarball https://github.com/NixOS/nixpkgs/archive/7d7622909a38a46415dd146ec046fdc0f3309f44.tar.gz) {});
in
pkgsA.mkShell {
  buildInputs = [
    pkgsA.ffmpeg_7-full
    pkgsB.jq
  ];
}'

2. Using nix-shell with shell.nix

2.1 What is shell.nix?

TODO: Come up with a beginner friendly intro that can be embedded later into the official docs, because they are lacking at the moment:

2.2 Issues & solutions

2.2.1 nix-shell messes with locales

Had this issue with a more elaborate shell.nix that sets up a Phoenix web project with a PostgreSQL instance (e.g., configures common options, creates tables, does the migrations), and the database kept failing with locale issues.

The solution is from this gist referring to Nixpkgs issue #318: nix-shell sets unsets LANG and friends, specifically to this comment:

The reason it doesn’t work is that you need to set $LOCALE_ARCHIVE, for instance by adding

LOCALE_ARCHIVE = "${pkgs.glibcLocales}/lib/locale/locale-archive";

to the shell expression. (Maybe we should pass LOCALE_ARCHIVE even in --pure mode, but I’m not sure.)

2.2.2 How to make nix-shell clean up after itself?

Use trap. It’s a shell built-in; see more on it by typing help trap.

2.x Examples

7 Likes

Hi,

Thanks for this.

I am still new to this. But since I am new, I started with nix flake dev shells already. What would be the reason for us new comers using nix-shell for new projects instead of nix flake dev shells?

1 Like

Pinning nix-shell -p lines is much less cumbersome with -I: Ad hoc shell environments — nix.dev documentation

2 Likes

Um, there is probably no reason:) I simply started writing about nix-shell because that’s what I ever worked with. Haven’t even tried flakes yet so please consider adding your experiences once this turns into a wiki post!

Thanks! Not sure how I missed this - and there is probably a lot more… Will add it as soon as I can.