Nix

Recommended reading order

  1. Nix — what Nix is, the core insight, the three layers (you are here)
  2. Nix Language — syntax: attribute sets, functions, let/in, strings, paths, import
  3. Nix Evaluation Model — semantics: thunks, laziness, forcing, closures, import nixpkgs {} chain
  4. Nix Store and Derivations — what evaluation produces: .drv files, the store, the build sandbox
  5. nixpkgs — the package repository, channels vs flakes, callPackage
  6. Nix Flakes — the modern project structure: inputs, outputs, lockfile, follows
  7. NixOS Module System — how NixOS merges modules: fixed-point, priorities, mkIf, types

What Nix is

Nix is three things layered on top of each other:

The four layers of the Nix ecosystem. Each layer builds on the one below it.

  • The Nix Language is a small, lazy, purely functional DSL (domain-specific language — a language designed for one task, not general programming). Its only job is to produce derivations — build plans.
  • The build system takes those derivation descriptions, executes them in a sandbox, and writes outputs to the Nix store.
  • The package manager is the user-facing layer: evaluates Nix expressions, decides what needs building, manages profiles (sets of installed packages), handles garbage collection.

The problem it solves

Traditional Unix package managers (apt, rpm, pacman) deploy packages into a global, mutable namespace:

/usr/lib/libssl.so.1.1    ← only one version can exist here
/usr/bin/python3           ← one python3
/usr/share/foo/            ← one foo

Eelco Dolstra’s PhD thesis (Utrecht University, defended 2006; the seminal conference paper appeared at USENIX LISA 2004 as “Nix: A Safe and Policy-Free System for Software Deployment”) identified the pathologies this creates:

ProblemWhat happens
Destructive upgradeInstalling v2 of a library overwrites v1, potentially breaking other programs that needed v1
DLL hellProgram A needs libfoo 1.x, Program B needs libfoo 2.x — impossible to satisfy simultaneously in a single /usr/lib/
Incomplete deploymentPackage was built on a machine with library X installed implicitly; it ships without declaring X as a dependency; works on the builder, breaks on fresh machines
No atomic rollbackUpgrade fails halfway through → system in inconsistent state
No reproducibilityTwo machines with “the same packages installed” diverge over time

The core insight

Dolstra’s fix: treat software deployment as a purely functional computation. The installed package should be a deterministic function of its inputs (source code + all dependencies + build instructions + build tools). If the function is pure:

  • Same inputs always produce the same output (reproducibility)
  • Multiple versions coexist because different inputs produce different output paths (no DLL hell)
  • Rollback is trivial — just switch which output path you’re using (no destructive upgrades)
  • Outputs can be cached and shared between machines (binary caches)

This insight directly explains every design choice:

  • The store uses content-addressed paths so different versions don’t collide
  • The language is pure and lazy so expressions describe builds without side effects
  • The build sandbox enforces purity by hiding anything not explicitly declared as an input

The unified mental model

The two-phase Nix pipeline: evaluation (pure, produces .drv build plans) followed by building (sandboxed, produces store paths). A binary cache can short-circuit the build phase.

One nuance: nixpkgs is not just packages. It also contains the entire NixOS module library (all those services.*.enable options), lib (utility functions), and stdenv (the standard build environment). The NixOS manual and the nixpkgs manual document the same repository from different angles.

See also