Nix’s develop program starts a shell with the results of a derivation. For example, to start a bash shell with Cargo on a machine that doesn’t have it installed:
nix develop nixpkgs#cargo
bash-5.2$
The real use case of develop is building development environments, which allow setting up a shell with multiple dependencies.
Nix’s template directory has some useful and community-maintained examples to get started building development environments. For example, the Rust template has a flake that sets up everything needed in a Rust project:
{ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/92d295f588631b0db2da509f381b4fb1e74173c5"; utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, utils }: utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; in { devShell = with pkgs; mkShell { buildInputs = [ darwin.apple_sdk.frameworks.Security libiconv gcc cargo rustc rustfmt rustPackages.clippy rust-analyzer ]; RUST_SRC_PATH = rustPlatform.rustLibSrc; }; } ); }
After saving it as flake.nix
in a project’s directory1, running nix develop
starts the shell with everything available.
To skip the shell and run a one-off command, use the --command
flag:
nix develop --command cargo --version
cargo 1.77.1
Even easier; to automatically load the environment when entering the project’s directory, use direnv.
Create a file named .envrc
containing the use flake
directive:
use flake .
Then, run direnv allow
in the project directory, and all dependencies are added to the current shell.
When switching to another directory, the dependencies are unloaded until you return.
After checking in the flake, the .envrc
file, and the generated flake.lock
, the project’s dependencies are automatically installed and version locked, resulting in a reproducable setup for the project.
On-demand development environments
Managing environments with Nix is powerful, but a downside of this approach is that the flake file needs to be checked into version control. That’s not a problem for projects that use Nix to manage their dependencies, but, when working on a project you don’t own, adding another way to handle dependencies might not be appreciated by the other maintainers. Aside from that, it might be useful to share development environments between similar projects without having to duplicate the flake.
Luckily, both Nix and direnv allow dependencies to be loaded from other paths than the current directory.
To start a development shell from a flake in the ~/devshells/rust
directory, pass the directory path to the call to nix develop
command:
nix develop ~/devshells/rust --command cargo --version
cargo 1.77.1
To use a flake from outside the current directory with direnv, add a path to the directory containing the flake in the .envrc
file:
use flake ~/devshells/rust
This means that just having an .envrc
file that points to a flake located elsewhere is enough to handle dependencies.
This still requires a single file to be added to the project directory, but it allows for moving the flake and lock file to a seperate, version-controlled, location.
A repository of development environments
For projects I can’t add flakes to, I use my own repository of development environments2, which includes flakes for to set up the following languages and utilities:
- Rust
- version 1.77.1, with Cargo, rustfmt, Clippy, and rust-analyzer
- Rustup
- version 1.26.0, a copy of the Rust flake, with with Rustup instead of separate utilities for projects that depend on it
- Elixir
- version 1.16.2 on Erlang 25.3.2.11
- Node.js
- version 22.0.0, with Prettier 3.2.5
- PostgreSQL
- version 15.6, with
PGDATA
configured to be directory-local - Ruby
- version 3.3.1
This means adding a single-line .envrc
is enough to add a develoment environment for Rust projects:
use flake ~/devshells/rust
This takes the flake file from the rust directory in my local checkout3 of my development environment repository.
Because environments can be environments can be layered, a Phoenix project requiring Elixir, Node.js and PostgresQL simply stacks three flakes:
use flake ~/devshells/elixir use flake ~/devshells/nodejs use flake ~/devshells/postgresql
After adding the flake, ensure it’s checked into version control. If not, Nix can’t find it and will throw an error message that doesn’t quite explain what’s wrong:
error: getting status of '/nix/store/0ccnxa25whszw7mgbgyzdm4nqc0zwnm8-source/flake.nix': No such file or directory
↩︎Other repositories with development environment exist, like the aforementioned NixOS/templates and the-nix-way/dev-templates. One could point a project’s
.envrc
file directly to one of these and get a working environment. I’ve done that in the past, and will certainly continue doing so.However, if I have to return to a project frequently, I prefer setting up my own development shell and running from that. Preparing one myself ensures the shell doesn’t include anything that’s not needed for my projects, and makes any issues that arise easier to debug.
Still, these repositories are a great starting point for writing your own development shells. My Rust shell, for example, is based on the the Rust flake from NixOS/templates.
↩︎Instead of using a local checkout, you could also point the
.envrc
file directly to a file on GitHub, for example:use flake github:jeffkreeftmeijer/devenv?dir=rust
This makes the setup more portable, but removes the ability to use and update lock files. Since I prefer my development environments to be version-locked and infrequently updated, that’s a dealbreaker for me.
↩︎