How to Set Up Nix Shell for Development Environments
Nix is known for its robust package management and reproducibility. One of its standout features is the Nix Shell, which allows you to create isolated, reproducible environments for your development projects. Whether working on a small script or a full-fledged application, setting up a Nix Shell ensures consistency across different environments.
Why Use Nix Shell?
Setting up consistent environments for team members when developing both front-end and back-end systems was a real pain back in the day. We often juggled multiple Node.js versions and managed countless dependencies across projects. Our full-stack engineers would spend about 10 minutes each time they switched from front-end to back-end to ensure no conflicting versions of dependencies. With Nix Shell, this problem is eliminated thanks to its ability to create project-specific environments, ensuring consistency and saving time.
Before diving into the setup, here’s a quick look at why you’d want to use Nix Shell:
- Isolated Development Environments: Nix Shell allows you to create environments that won’t interfere with your system’s global packages.
- Reproducibility: Anyone using your project can easily set up the same environment, ensuring consistency.
- Portability: You can easily define development environments that are project-specific, making it easy to collaborate on code. All of them are stored inside one single file.
Now, let’s get started with the setup.
Step 1: Install Nix
If you haven't already installed Nix, you'll first need to set it up. Follow these steps:
- Open your terminal.
Run the following command to install Nix:
$ curl -L https://nixos.org/nix/install | sh
- After installation, source your environment:
$ . ~/.nix-profile/etc/profile.d/nix.sh
You can also follow the official installation guide if you prefer a more detailed approach.
Step 2: Create a shell.nix
File
The shell.nix
file defines the environment and dependencies for your project.
Let's create one for a simple Deno + Typescript project.
- Inside your project directory, create a new file named
shell.nix
$ touch shell.nix
- Edit the file with your preferred text editor and define your environment.
Below is an example of a Typescript-based project:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
name = "Typescript Project";
buildInputs = with pkgs; [
nodejs
typescript
deno
];
# Optionally, define environment variables
shellHook = ''
FOO=BAR
echo "This is my Typescript Project."
'';
}
In this example, I'm importing the Nix package set (nixpkgs
) and specifying nodejs as our environment, along with typescript
and deno
. The shellHook
runs a command every time you enter the shell; it's a good idea you can source your project's .env
file here without leaking your credentials.
Step 3: Enter the Nix Shell
Once I've defined the environment in shell.nix
, entering the shell is as simple as one command:
$ nix-shell
This command will download the necessary packages and drop me into the isolated environment defined in shell.nix
. You'll see the message from the shellHook
if you set one. You can work with your project from here, knowing that the environment is isolated and all dependencies are correctly set up.
Step 4: Working with Development Shells
While nix-shell
is perfect for creating temporary environments, sometimes you may want to ensure the environment is more tailored for development purposes.
This is where devShells
in a flake.nix
file comes into play.
Here is a quick way to turn your environment into a reproducible development shell using Flakes:
- Ensure your
nix
command can work with flakes by enabling them in your configuration.
$ nix --experimental-features 'nix-command flakes'
- Create a
flake.nix
File into the project root:
{
description = "My development shell";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }: {
devShells.default = nixpkgs.lib.mkShell {
buildInputs = [
nixpkgs.nodejs
nixpkgs.typescript
nixpkgs.deno
];
};
};
}
- Now, you can enter the development shell with the following:
$ nix develop
This approach ensures that everyone using the project has the same environment by referencing the same flake.nix
file.
Step 5: Customising Your Nix Shell
We can further customise our Nix shell to fit our project's needs:
- Add more dependencies: In the
buildInputs
section, you can add other languages, libraries, and tools likenodejs
,git
,docker
, etc... - Environment variables: You can set up project-specific environment variables inside
shellHook
. - Pre-run scripts: The
shellHook
can also execute scripts such as initialising databases or setting up cache directories.
Step 6: Exiting the Shell
When you're done working in your environment, type exit
to leave the Nix shell. It doesn't affect your global system environment, so cleaning up afterwards is unnecessary.
Conclusion
Nix Shell provides a flexible, isolated development environment that ensures consistency across teams and systems by setting up a shell.nix
or flake.nix
, you can define precisely how your project's environment should look, making onboarding new developers easier and ensuring that everyone works with the same tools and dependencies.
With just a few steps, you can have a fully reproducible development environment that can be shared across teams and systems.
This post is based on verified information from the Nix manual. If you need more details, the documentation provides in-depth coverage of nix-shell
and Flakes.
Bonus
Multiple System Architecture flake.nix
for Project
Managing environments can be a hassle when working on cross-platform projects with developers using different operating systems. Nix simplifies this by letting you define shared dependencies and OS-specific configurations in a single flake.nix
file, reducing duplication and complexity. This method ensures each developer’s setup remains consistent, whether on macOS, Linux, or Windows via WSL. Here's a streamlined example:
{
description = "Cross-platform project for macOS and Windows (WSL) with shared dependencies";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # Specify the Nixpkgs source
};
outputs = { self, nixpkgs }: let
# Function to define the development shell for each system
mkDevShell = system: pkgs: pkgs.mkShell {
buildInputs = [
pkgs.python310 # Python 3.10
pkgs.python310Packages.pip
pkgs.nodejs-18_x # Node.js 18.x
pkgs.yarn # Yarn for Node.js package management
];
shellHook = ''
echo "Welcome to the ${system} dev environment!"
echo "Python version: $(python --version)"
echo "Node.js version: $(node --version)"
'';
};
# Define packages for each system
darwinPkgs = import nixpkgs { system = "aarch64-darwin"; }; # macOS (ARM)
linuxPkgs = import nixpkgs { system = "x86_64-linux"; }; # Windows 11 via WSL (Linux x86_64)
in {
# Use mkDevShell to define the environments for both macOS and WSL
devShells.aarch64-darwin.default = mkDevShell "macOS" darwinPkgs;
devShells.x86_64-linux.default = mkDevShell "Windows (WSL)" linuxPkgs;
};
}
This approach keeps multi-platform development simple and ensures consistency across different environments.