Nix
Recommended reading order
- Nix — what Nix is, the core insight, the three layers (you are here)
- Nix Language — syntax: attribute sets, functions, let/in, strings, paths, import
- Nix Evaluation Model — semantics: thunks, laziness, forcing, closures,
import nixpkgs {}chain- Nix Store and Derivations — what evaluation produces: .drv files, the store, the build sandbox
- nixpkgs — the package repository, channels vs flakes,
callPackage- Nix Flakes — the modern project structure: inputs, outputs, lockfile,
follows- 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:
| Problem | What happens |
|---|---|
| Destructive upgrade | Installing v2 of a library overwrites v1, potentially breaking other programs that needed v1 |
| DLL hell | Program A needs libfoo 1.x, Program B needs libfoo 2.x — impossible to satisfy simultaneously in a single /usr/lib/ |
| Incomplete deployment | Package 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 rollback | Upgrade fails halfway through → system in inconsistent state |
| No reproducibility | Two 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
- Nix Language — syntax and semantics of the expression language
- Nix Store and Derivations — the build model, store paths, and the evaluation/build split
- nixpkgs — the package repository
- Nix Flakes — the modern way to pin dependencies and structure projects