TL;DR:

  • Nix Flakes pin every dependency — language versions, system libraries, CLI tools — to exact hashes, making nix develop reproduce the same environment on any machine
  • flake.nix is 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:

  1. Start with a template: nix flake init with community templates (nix flake init -t github:nix-community/nix-flake-templates#...) gives you working starting points
  2. Use nix search: nix search nixpkgs nodejs finds the right package name
  3. Lean on devenv: The devenv.sh project 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.