Free — no signup required

Nix: The Reproducibility Engine

4 min read

The Problem Docker Doesn't Fully Solve

Docker images are excellent, but they have a subtle reproducibility gap. When you write RUN apt-get install -y curl in a Dockerfile, you are asking for "whatever version of curl is current in the apt repository today." Build the same Dockerfile six months later and you may get a different version of curl, or a different version of one of curl's transitive dependencies. The image is not bit-for-bit reproducible over time.

Nix solves this at a deeper level. It is a package manager (and optionally a full operating system, NixOS) built on a single insight borrowed from functional programming: a package is a pure function of its inputs. Given the same inputs — source code, compiler version, build flags, and all transitive dependencies — you always get the same output. Always.

How Nix Stores Packages

In a traditional Linux system, installing Python puts it at /usr/bin/python. If you install a different version, it overwrites the first. Conflict.

Nix never installs anything to /usr/bin. Instead, every package lives in the Nix store at a path that includes a cryptographic hash of its entire dependency tree:

/nix/store/b6gvzjyb2pg0kjf...-python-3.11.0/bin/python
/nix/store/a1c3d9xk7m2nqr8...-python-3.9.18/bin/python

The hash b6gvzjyb2pg0kjf... is computed from the Python 3.11.0 source, the specific GCC version used to compile it, the version of glibc it links against, and every other input. If any input changes by a single byte, the hash changes, and Nix treats it as a completely different package. This means:

  • 50 versions of Python can coexist on the same machine with zero conflicts.
  • If two builds produce the same hash, they are guaranteed identical. No "works on my machine" — the hash is the proof.
  • Rollbacks are trivial. Switching to a previous environment means pointing your $PATH at a different set of /nix/store paths.

Nix Flakes: Reproducible Development Shells

For development environments, the key Nix feature is nix develop (using Nix Flakes). Instead of spinning up a full Docker container, Nix modifies your current shell's $PATH to include only the specific tools you declared — and nothing else.

A flake.nix file describes the environment:

{
  description = "Go + Terraform development environment";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
  };

  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      devShells.${system}.default = pkgs.mkShell {
        buildInputs = [
          pkgs.go_1_21
          pkgs.terraform
          pkgs.kubectl
          pkgs.awscli2
        ];

        shellHook = ''
          echo "Dev environment ready."
          echo "Go: $(go version)"
          echo "Terraform: $(terraform version | head -1)"
        '';
      };
    };
}

Running nix develop in a directory containing this file:

  1. Nix resolves the exact versions of go, terraform, kubectl, and awscli2 pinned by the nixpkgs commit referenced in flake.lock.
  2. It downloads only the specific binaries needed (not a full OS image).
  3. It spawns a new shell with $PATH modified to include those binaries.
  4. When you type exit, your shell returns to normal. Nothing was installed globally.

The flake.lock file — automatically generated and committed to version control — pins the exact nixpkgs commit used. Anyone who runs nix develop against the same flake.lock gets byte-for-byte identical tools, regardless of when they run it or what OS they are on.

The Nix Learning Curve

Nix uses its own purely functional language (also called Nix) to describe packages and environments. The syntax is unfamiliar, the error messages are cryptic, and the concepts (derivations, closures, overlays) take significant time to internalize. This is not a tool you pick up in an afternoon. Most teams adopt Nix gradually — starting with nix-shell for simple use cases before moving to full Flakes.

Interview Tip

A common interview question in platform engineering roles is: "What's the difference between Docker and Nix for managing development environments?" The key distinction is the layer at which they operate. Docker provides OS-level isolation via Linux namespaces and cgroups — you get a full filesystem and process tree. Nix operates at the package graph level — it modifies $PATH without any OS virtualization. Docker is heavier but more portable (works on any Docker host). Nix is lighter and more mathematically reproducible, but requires Nix to be installed on the host. The sophisticated answer notes that many teams use both: Nix inside a Dev Container to get the portability of Docker with the reproducibility of Nix.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free