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:
- Configuration Drift: System cleanliness was no longer guaranteed because programs were simply installed in the NixOS configuration or Home Manager.
- 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 A: Requires Python 3.9 and Node.js 16
- Project B: Requires Python 3.11 and Node.js 20
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
- Enter project directory: As soon as I run
cd projekt-a, direnv automatically activates the environment - Isolated tools: Python 3.9 and Node.js 16 are ONLY available in Project A
- No conflicts: In Project B I can have completely different versions
- Reproducible: Anyone with this
flake.nixgets exactly the same environment
Advantages over Docker
- ✅ No container overhead: Native performance
- ✅ Instant: Environment activates immediately on
cd - ✅ Lightweight: No images that are GB in size
- ✅ IDE integration: Tools are directly available, no remote container needed
- ✅ Declarative: Everything defined in one file, no build process
- ✅ Deterministic: flake.lock guarantees reproducible builds - unlike Dockerfiles
- ✅ Memory efficient: With Docker, everything is reinstalled in each container/image. Nix only creates symlinks from the Nix store - each tool exists only once physically (more on this in a future article)
This Solves Both Problems
- Configuration Drift: Programs are stored in the Nix store but are only available in the project context
- 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.