Nix Reproducible Dev Environments with Flakes: Complete Setup Guide

Nix Reproducible Dev Environments with Flakes

Nix reproducible dev environments have emerged as the gold standard for teams that are tired of “works on my machine” problems. Unlike Docker-based dev containers that abstract the OS, Nix provides bit-for-bit reproducible toolchains that run natively on your machine. With Nix Flakes — the modern interface for Nix — you get lockfiles, composability, and a dramatically improved developer experience.

This guide walks you through setting up Nix Flakes for real-world projects, from a simple Node.js app to a polyglot microservices repository. You will learn how to define dev shells, pin dependencies, integrate with CI pipelines, and avoid common pitfalls that trip up Nix newcomers.

Why Nix Over Docker Dev Containers

Docker dev containers run your tools inside a Linux container, which means you lose native macOS/Windows integration — file watching is slow, GUI tools do not work directly, and bind mount performance is poor. Nix takes a fundamentally different approach by installing packages into an immutable store (/nix/store) and creating isolated shell environments that reference those packages.

The result is native performance, instant shell startup, and complete reproducibility across machines. Moreover, every dependency is tracked by a cryptographic hash, so two developers with the same flake.lock are guaranteed to have identical tools — down to the exact compiler version and shared library paths.

Nix reproducible dev environments server infrastructure
Reproducible infrastructure powered by declarative Nix configurations

Installing Nix and Enabling Flakes

Start by installing Nix using the Determinate Systems installer, which enables Flakes by default and works on macOS, Linux, and WSL2:

# Install Nix with Flakes enabled (recommended installer)
curl --proto '=https' --tlsv1.2 -sSf \
  -L https://install.determinate.systems/nix | sh -s -- install

# Verify installation
nix --version
# nix (Nix) 2.21.0

# Check Flakes support
nix flake --help

If you already have Nix installed without Flakes, add experimental features to your configuration:

# ~/.config/nix/nix.conf
experimental-features = nix-command flakes

Your First flake.nix

A Flake is defined by a flake.nix file at the root of your project. It declares inputs (dependencies like nixpkgs) and outputs (dev shells, packages, etc.). Here is a practical example for a Node.js + Python project:

{
  description = "Full-stack web application dev environment";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.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; [
            # Node.js toolchain
            nodejs_20
            nodePackages.pnpm
            nodePackages.typescript

            # Python toolchain
            python312
            python312Packages.pip
            python312Packages.virtualenv

            # Database tools
            postgresql_16
            redis

            # Dev utilities
            jq
            httpie
            just  # command runner
          ];

          shellHook = ''
            echo "Dev environment loaded!"
            echo "Node: $(node --version)"
            echo "Python: $(python --version)"
            echo "PostgreSQL: $(psql --version)"
          '';

          # Environment variables
          DATABASE_URL = "postgresql://localhost:5432/myapp_dev";
          REDIS_URL = "redis://localhost:6379";
        };
      });
}

Run nix develop in the project directory and you will have all tools available instantly. The first run downloads everything; subsequent runs are instant because packages are cached in /nix/store.

# Enter the dev shell
nix develop

# Or run a single command in the shell
nix develop --command just test

# Generate the lockfile (committed to git)
nix flake lock

Nix Flakes for Polyglot Monorepos

For monorepos with multiple services, define multiple dev shells in a single flake. This approach lets each team member enter only the shell they need, without downloading unnecessary toolchains.

{
  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = nixpkgs.legacyPackages.${system}; in
      {
        devShells = {
          default = pkgs.mkShell {
            buildInputs = with pkgs; [ just git ];
          };

          backend = pkgs.mkShell {
            buildInputs = with pkgs; [
              jdk21
              gradle
              postgresql_16
            ];
          };

          frontend = pkgs.mkShell {
            buildInputs = with pkgs; [
              nodejs_20
              nodePackages.pnpm
              playwright-driver.browsers
            ];
          };

          infra = pkgs.mkShell {
            buildInputs = with pkgs; [
              terraform
              kubectl
              helm
              awscli2
            ];
          };
        };
      });
}
# Enter specific shells
nix develop .#backend
nix develop .#frontend
nix develop .#infra
Cloud infrastructure development workflow
Managing polyglot development workflows with declarative Nix shells

Integrating with direnv for Automatic Shell Activation

Typing nix develop every time you enter a directory is tedious. The nix-direnv integration automatically activates your dev shell when you cd into the project, and deactivates it when you leave. This is the recommended approach for daily development.

# Install direnv (add to your global Nix profile or system config)
nix profile install nixpkgs#direnv nixpkgs#nix-direnv

# Add to your shell rc file (~/.bashrc or ~/.zshrc)
eval "$(direnv hook bash)"  # or zsh

# In your project root, create .envrc
echo "use flake" > .envrc
direnv allow

Now every time you cd into the project directory, your tools are available. The shell loads in milliseconds because nix-direnv caches the environment profile.

CI/CD Integration with Nix Flakes

The true power of Nix reproducible dev environments shines in CI/CD. Your CI pipeline uses the exact same toolchain as local development — no more drift between local and CI environments.

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: cachix/install-nix-action@v26
        with:
          extra_nix_config: |
            access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}

      - uses: cachix/cachix-action@v14
        with:
          name: my-project
          authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}

      - name: Run tests
        run: nix develop --command just test

      - name: Build
        run: nix build

Cachix provides a binary cache so your CI does not rebuild packages from source every run. Consequently, CI times drop from 15+ minutes to under 2 minutes after the first cached run.

When NOT to Use Nix

Nix has a steep learning curve, and the functional language can be intimidating for teams unfamiliar with it. If your team already has a working Docker Compose setup and nobody wants to learn Nix, forcing adoption will cause more friction than it solves. Additionally, Nix on native Windows (without WSL2) has limited support.

For simple projects with a single language and runtime (e.g., a basic Node.js app), a .tool-versions file with asdf or mise may be sufficient. Nix shines most in polyglot environments, complex build chains, and organizations that value byte-for-byte reproducibility across dozens of developers.

DevOps developer environment setup
Developer workstation with reproducible toolchain configuration

Key Takeaways

  • Nix reproducible dev environments guarantee identical toolchains across all developer machines and CI/CD pipelines
  • Nix Flakes add lockfiles and composability, making Nix practical for production teams
  • Use nix-direnv for automatic shell activation — it eliminates the friction of manual nix develop commands
  • Multiple dev shells in a single flake support polyglot monorepos without wasting disk space
  • Cachix binary caches make CI/CD fast by avoiding redundant package builds

Related Reading

External Resources

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top