Why Adopt Nix devShells for Reproducible Environments
The Perfect Development Environment, Finally Reproducible
After years of fighting with version conflicts, environments that diverge between colleagues, and endless "it works on my machine" scenarios, I discovered Nix. And for me, it's a revolution in development environment management.
Unlike traditional package managers I've tried them all (Homebrew, apt, conda, nvm...), Nix takes a purely functional approach that finally guarantees build and environment reproducibility, from laptop to CI to production.
What Exactly is Nix?
Nix is simultaneously:
- A cross-platform package manager (Linux, macOS, WSL)
- A development environment manager (devShells)
- A declarative build system (flakes and derivations)
Its key feature: each package (and environment) is built in a unique path in the Nix store, derived from all its build dependencies. Two builds with the same inputs produce the same outputs, at the same path — that's the foundation of reproducibility.
My Breakthrough with devShells
DevShells are per-project development environments, described in Nix, that you activate on demand. For me, they solved four daily struggles I'd been living with for years.
- Reliable Isolation Without Hacks
- Each repo describes its tool dependencies (compilers, CLIs, linters, SDKs) and their versions.
- Activation writes nothing to your global system, doesn't pollute your
/usr/local
, and doesn't break other projects. - Switching between projects is as simple as changing directories.
- Locked and Shared Versions
- The
flake.lock
file precisely freezes the inputs used. The entire team works on the same versions. - CI consumes the same inputs, avoiding "it passes on my CI but not on yours".
- Lightning-Fast Onboarding
- Clone →
nix develop
→ ready to go. No more lengthy "Installation" sections in the README. - The shell can expose team commands (tests, lint, build) and configure common environment variables.
- Polyglot and Cross-Platform
- Python, Node, Go, Rust, Java... coexist cleanly through a single mechanism.
- The same definitions work on macOS, Linux, and WSL.
My Daily Before/After
Before Nix:
- Debug for hours a project that won't build after a system update
- Spend half a day installing a new project's environment
- Scour GitHub issues to understand why "it works for him but not for me"
With Nix:
- Never again Node 18 vs 20 incidents, or OpenSSL 1.1 vs 3
- New team members are operational in minutes, not hours
- Team commands finally become memorable (
task test
,task fmt
,task check
)
Perfect Isolation: No More Clashing Dependencies
Thanks to flakes and direnv
/nix-direnv
, each project precisely defines its tools and libraries. When entering the folder, the appropriate environment is automatically loaded, without polluting the global system.
What I've been able to do concretely, in parallel on my machine:
- GCC 11 for a legacy project and GCC 13 for a recent project
- Python 2.7 + Node.js 14 for an old project and Python 3.12 + Node.js 20 for a new one
- Distinct versions of OpenSSL, databases, etc.
No more global system version headaches!
Minimal Example with Flakes + direnv
flake.nix
:
{
description = "Minimal Nix devShell";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
outputs = { self, nixpkgs }: let
system = "x86_64-darwin"; # or x86_64-linux / aarch64-darwin
pkgs = import nixpkgs { inherit system; };
in {
devShells.${system}.default = pkgs.mkShell {
buildInputs = [ pkgs.nodejs_20 pkgs.python311 pkgs.git ];
# Optional environment variables and hooks
shellHook = ''
echo "[devshell] Node: $(node -v), Python: $(python3 --version)";
export PIP_DISABLE_PIP_VERSION_CHECK=1
export UV_NO_CACHE=1
'';
};
};
}
.envrc
:
# Option A: basic direnv
use nix
# Option B (recommended): nix-direnv for instant activations
# use flake
Then:
direnv allow # execute once
Subsequent activations are instant with nix-direnv
(shell caching).
Multi-Version Without Compromise
Nix allows simultaneous installation of multiple versions of the same package, without hacks. This "time travel" is ideal for maintaining legacy projects while developing with the latest versions.
Reproducibility Everywhere: Local, CI, and Ephemeral Environments
With declarative definitions and the lock file (flake.lock
), I have the guarantee that the environment will be identical wherever I use it:
Locally: My MacBook, a colleague's, my Linux VM - same versions, same behavior.
In CI: GitHub Actions, GitLab CI, doesn't matter - CI uses exactly the same tools as I do locally.
In ephemeral environments: The most impressive is with tools like Devin.ai. I can share my flake.nix
and the AI instantly has the same environment as me, without manual installation, without complex setup.
Result: no more "it works locally but not in CI", no more time wasted synchronizing versions between developers and automated systems.
Simplified Team Collaboration
Near-instant onboarding:
- Clone the repository
nix develop
- Code immediately
This is actually the approach we adopted as a team: I started by proposing a simple flake.nix
on a pilot project, without forcing anyone. Result: progressive and natural adoption, without friction.
A Unified Package Manager
Nix effectively replaces a myriad of tools:
- System: MacPorts/Homebrew
- JavaScript: npm/yarn
- Python: pip/conda
- Ruby: gem
- Editor/CLI: vim/neovim plugins, VS Code, tmux, fish/zsh
One command to install everything on a new machine, one logic for all stacks.
Remote Development and Ephemeral Environments
Development servers: No more begging the system admin to install the right version of Node or Python. I deploy my flake.nix
, and the environment is identical to my laptop.
With AIs like Devin: This is where it gets magical. I share my Nix environment, and the AI instantly has the same setup as me. No "how to install X", no environment differences.
In CI/CD: No more version problems between my machine and CI. Same Nix, same environment, same results.
A Powerful Tool Ecosystem
- direnv + nix-direnv: automatic activation and shell caching
- Home Manager: dotfiles and user preferences declaratively, portable between machines
- Flakes: composition, reproducibility, integrated lockfile
Useful complements:
- Cachix: team binary cache, installs even faster and identically
devenv.sh
(Cachix): ergonomic overlay for local services (DB, queues) and team scriptsprocess-compose
: orchestration of multiple dev processes in the shell
Challenges and Considerations
- Real learning curve: it took me a few weeks to get comfortable with the terminology (derivations, store, flakes).
- Sometimes confusing documentation: rich but scattered, I often learned by example.
- Progressive adoption: I started with a single project, then gradually extended.
Despite this, my return on investment has been clear in productivity and peace of mind.
Common Pitfalls and How to Avoid Them
- "Too impure" shells: if you depend on external states (local daemons, sockets, secrets), document them. For CI, favor the most hermetic shells possible.
- Large closures: use a team binary cache (Cachix/Nix server) to avoid rebuilding.
- macOS/Linux variants: structure your flakes to generate shells by
system
(e.g., viaflake-utils
).
Growing Adoption
The Nix ecosystem exceeds 120,000 packages. More and more teams I meet are adopting it, whether in production or just to standardize their dev environments.
Conclusion: Why I Adopted Nix
For me, Nix has been a game changer. No more "it works on my machine", no more hours lost debugging version conflicts, no more painful onboarding.
Today, I can't imagine starting a project without a Nix devShell. It's become as natural as creating a README.
Yes, I spent a few weekends understanding the concepts — but the daily peace of mind is invaluable. And when a colleague tells me "your project doesn't work on my machine", I know it's just because they haven't done nix develop
yet 😉