TL;DR:
- Nix Flakes pin every dependency — language versions, system libraries, CLI tools — to exact hashes, making
nix developreproduce the same environment on any machine flake.nixis a single file that replaces.nvmrc,.python-version, Brewfiles, and most of your Docker dev containers- The learning curve is real but the payoff — zero-friction onboarding, identical CI and local environments, no more “but it works on my machine” — is worth it for teams
Every developer has lost hours to environment drift: a library version mismatch that makes tests fail on one machine and pass on another, a tool that works on macOS but not on the CI Linux runner, onboarding a new teammate who spends two days getting their environment right. These aren’t edge cases. They’re the chronic low-grade friction of software development.
Nix and Nix Flakes address this at the root: instead of telling developers which tools to install and hoping they install the right versions, you describe the exact environment as code and Nix guarantees it.
What Nix Flakes Actually Are
Nix is a package manager and build system that treats packages as pure functions: given a specific set of inputs (source code + dependencies), you always get the same output, and nothing can sneak in from the ambient system to change the result. Packages are stored in the Nix store at /nix/store/ with content-addressable paths — if two packages have the same hash, they’re the same package.
Flakes are the modern way to use Nix. They standardise how Nix projects declare their inputs (other Nix packages, libraries, tools) and outputs (packages, development shells, NixOS configurations). The key innovation: flake.lock pins every input to an exact git commit hash, making the entire dependency tree reproducible.
For development environments specifically, flakes let you define a devShell — a shell environment with exact versions of every tool you need — that anyone can enter with nix develop.
Installing Nix with Flakes Enabled
The Determinate Systems installer is the recommended path for macOS and Linux in 2026 — it handles macOS-specific setup correctly and enables flakes by default:
# Install Nix with flakes enabled (macOS and Linux)
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
# Verify installation
nix --version
# nix (Nix) 2.25.x or similar
If you already have Nix installed without flakes, add this to ~/.config/nix/nix.conf:
experimental-features = nix-cli-experimental flakes
A Practical flake.nix for a Node.js Project
Here is a complete, working flake.nix for a Node.js project that pins Node.js, pnpm, and a few CLI tools:
{
description = "My Node.js project dev environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
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; [
nodejs_22 # Exact Node.js version
nodePackages.pnpm
nodePackages.typescript
git
jq
curl
];
shellHook = ''
echo "Node.js $(node --version) ready"
echo "pnpm $(pnpm --version) ready"
'';
};
}
);
}
To use it:
# Enter the dev environment (first run downloads packages)
nix develop
# Or use direnv (auto-activates when you cd into the directory)
echo "use flake" > .envrc
direnv allow
With direnv integrated, the environment activates automatically when you enter the directory and deactivates when you leave. No manual activation.
Python Project Example
{
description = "Python data science environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
python = pkgs.python313;
in
{
devShells.default = pkgs.mkShell {
buildInputs = [
python
python.pkgs.pip
python.pkgs.virtualenv
pkgs.uv # Fast Python package manager
pkgs.ruff # Linter/formatter
pkgs.postgresql_16 # Database for local dev
];
shellHook = ''
echo "Python $(python --version)"
# Create virtualenv if it doesn't exist
if [ ! -d .venv ]; then
uv venv .venv
fi
source .venv/bin/activate
'';
};
}
);
}
This pins Python 3.13 exactly — no more .python-version files that different tools interpret differently. Every developer and every CI runner gets the same Python.
Multi-Language Project
Many real projects mix languages. Nix handles this better than any other tool:
{
description = "Full-stack app: Go backend + React frontend";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
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; [
# Backend
go_1_24
golangci-lint
# Frontend
nodejs_22
nodePackages.pnpm
# Database
postgresql_16
# Dev tools
docker-compose
kubectl
k9s
jq
httpie
];
};
}
);
}
One flake.nix replaces your Brewfile, asdf configuration, volta configuration, and any other per-language version manager.
The flake.lock File
After your first nix develop, Nix generates flake.lock:
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1748000000,
"narHash": "sha256-abc123...",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a1b2c3d4e5f6...",
"type": "github"
}
}
}
}
This pins the exact git commit of nixpkgs. Anyone who runs nix develop with this flake.lock gets the exact same packages — not “the same version string” but the exact same binary artifacts, derived from the exact same source. Commit flake.lock to your repository alongside flake.nix.
To update all inputs to their latest versions:
nix flake update # Updates flake.lock to latest upstream
nix develop # Rebuild with new versions
CI Integration
In GitHub Actions:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main # Cache Nix store
- name: Run tests
run: nix develop --command pnpm test
The magic-nix-cache-action dramatically speeds up CI runs by caching the Nix store. Your first CI run builds everything; subsequent runs are fast.
Addressing the Learning Curve
Nix has a reputation for a steep learning curve, which is partially deserved. The full Nix language (a purely functional language) is unfamiliar, and the documentation is historically scattered.
For getting started, you need much less than the full language:
- Start with a template:
nix flake initwith community templates (nix flake init -t github:nix-community/nix-flake-templates#...) gives you working starting points - Use
nix search:nix search nixpkgs nodejsfinds the right package name - Lean on
devenv: Thedevenv.shproject provides a higher-level wrapper over Nix flakes with a more approachable configuration language — a good on-ramp before going full Nix
The investment pays off. Environment setup time for new team members drops from hours to minutes. “Works on my machine” bugs from environment drift become rare. CI and local environments run the same code. For teams doing serious software development, that’s worth the learning curve.