Skip to content
Go back

Project-Isolated Development Environments with direnv and Nix

TL;DR

direnv + nix creates project-isolated development environments without container overhead. Each project gets its own tool versions (e.g., Python 3.9 in Project A, Python 3.11 in Project B), which activate automatically when you cd into the directory. Unlike Docker: native performance, instantly available, deterministically reproducible via flake.lock. Solves configuration drift and dependency hell without installing programs system-wide.


The Problem: Configuration Drift and Dependency Hell

When I switched from Windows to NixOS, my goal was to gain full control over my machine and master system rot. While I now know exactly what I have installed, I still felt something was messy. Programs or tools I only needed for one project were simply installed system-wide or at the home level, which quickly led to problems as individual projects required different versions of the tools.

So I had two problems:

  1. Configuration Drift: System cleanliness was no longer guaranteed because programs were simply installed in the NixOS configuration or Home Manager.
  2. Dependency Hell: Installing different programs with different versions made dependencies unclear.

That’s when I discovered Dev Containers. All programs and tools are installed in this container, giving you a clean environment without having to install them system-wide. Even though Docker and containerization are wonderful, I don’t like the overhead inherent to Docker and containers. Quick changes require stopping and restarting the container, and the handling wasn’t for me.

So I kept looking and came across direnv + nix! With Nix, I could clearly define what I need for my project - which can sometimes be difficult with a Dockerfile. direnv then automatically activates the environment when entering the directory.

A Simple Example

Two projects:

Project Structure

projekt-a/
├── flake.nix          # Nix environment definition
├── flake.lock         # Automatically generated (locked dependencies)
├── .envrc             # direnv configuration
├── src/               # Project code
│   └── main.py
└── README.md

flake.nix for Project A

{
  description = "Development environment for Projekt A";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in
      {
        devShells.default = pkgs.mkShell {
          buildInputs = with pkgs; [
            python39      # Specifically Python 3.9
            nodejs_16     # Specifically Node.js 16
            poetry        # Python dependency manager
            git
          ];

          shellHook = ''
            echo "🚀 Projekt A environment loaded!"
            echo "Python: $(python --version)"
            echo "Node: $(node --version)"
          '';
        };
      }
    );
}

.envrc (direnv Configuration)

use flake

How It Works

  1. Enter project directory: As soon as I run cd projekt-a, direnv automatically activates the environment
  2. Isolated tools: Python 3.9 and Node.js 16 are ONLY available in Project A
  3. No conflicts: In Project B I can have completely different versions
  4. Reproducible: Anyone with this flake.nix gets exactly the same environment

Advantages over Docker

This Solves Both Problems

  1. Configuration Drift: Programs are stored in the Nix store but are only available in the project context
  2. Dependency Hell: Each project has its own isolated versions

Conclusion

For my local development, direnv + nix is a wonderful solution. The environments are fast, lightweight, and exactly reproducible. Nix runs on Linux, macOS, and via WSL2 on Windows too.

Docker dominates as the standard, and in larger teams, acceptance for Nix is often lacking. The learning curve and lower adoption rate make it harder to adopt. However, I feel that Nix is becoming increasingly popular and both tools can be combined.

Another advantage: With Nix, Docker images can be built deterministically and bit-for-bit reproducibly - unlike Dockerfiles. More on this in a future article.


Share this post on:

Previous Post
Nix – Ending the System Chaos