Skip to content

Motivation

Bastiat

  • Non-mainstream syntax
  • Laziness surprises
  • Poor error messages
  • Reading is easier than writing
  • Debugging can feel indirect
  • “Why can’t I just curl this thing?”
  • “Why is this forbidden?”
  • “Why do I need to declare this explicitly?”
  • Steep initial slope
  • Feels slower before it feels faster
  • Easy to feel blocked early on
  • documentation -> Could be better
  • ide support -> Could be better
FrustrationWhat It Enforces
No implicit depsExplicit dependency graphs
No global stateReproducibility
No networkHermetic builds
Immutable storeRollbacks & safety
Weird languageLaziness + composability

Nix charges you early for mistakes you normally pay for later.

Missing dependencycaught at build time, not prod
Undeclared toolcaught immediately, not on a coworker’s laptop
Driftimpossible, not “unlikely”
extensibilityadd/change functionality without touching old code

Nix doesn’t save time immediately. It stops time from leaking later.

Surely something else exists that is comparable. Right?

  • Docker Builds Are Filesystem-Based, Not Dependency-Based
  • Docker images are built as a stack of filesystem layers
  • Each instruction in a Dockerfile (RUN, COPY, ADD, etc.) creates a new layer

Layers record:

  • file additions
  • file deletions
  • file modifications

Docker does not understand:

  • why a file exists
  • which files are dependencies vs outputs
  • which tools were actually used
# Dockerfile
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl wget vim
RUN echo "Hello from Docker!" > /hello.txt
CMD ["cat", "/hello.txt"]
  • Produces a runnable, isolated filesystem snapshot
  • Ensures runtime parity between machines
  • Freezes results, not process
  • Deterministic builds over time
  • Reproducible images from the same Dockerfile
  • Explicit dependency graphs
  • Hermetic builds
  • Docker tracks what changed on disk, not what the build depends on.
  • Docker isolates well, but doesn’t integrate well
  • half tools on local machine, half the tools inside docker images

The industry keeps independently rediscovering the same problems — and inventing partial solutions inside narrower scopes.

Nix didn’t invent these concerns. It’s the first tool that tries to solve them generally, instead of per-language or per-runtime.

  • asdf / mise – great for tools, not system deps, no purity
  • devcontainers – good UX, still Docker-centric, heavy
  • bazel / pants – excellent builds, weak local dev ergonomics
  • ansible / chef / salt – fleet config, poor dev loop
  • homebrew + scripts – works… until it doesn’t
  • Non-deterministic installs (package.json alone was not enough)
  • “Works on my machine” caused by solver drift
  • Hidden transitive dependency changes
  • Slow, stateful installs
  • Lockfiles (yarn.lock) → explicit dependency graphs
  • Deterministic resolution → same tree everywhere
  • Content-addressed cache → reuse instead of rebuild
  • Offline installs → fewer hidden dependencies

Yarn is an admission that imperative package installs are not reliable

  • reproducibility matters
  • dependency graphs must be explicit
  • Only governs JavaScript dependencies
  • Assumes the system environment already works
  • compilers
  • native libraries
  • OS differences
  • build tools

sbt wasn’t meant to be “just Scala’s Maven.”

  • Incremental builds
  • Precise dependency tracking
  • Declarative build definitions
  • Programmatic builds (builds as code)
  • Cross-project composition
  • Sound familiar again?
  • Fine-grained incremental compilation
  • Task graphs
  • Dependency-aware caching
  • Build definitions as Scala code
  • Bazel
  • Pants
  • Buck
  • Gradle
  1. Host Environment Leakage
  2. Non-Hermetic Builds
  3. Scope Explosion
  • Hard to reason about
  • Hard to standardize
  • Hard to cache globally

Haskell learned to treat dependency updates like a pipeline, not a fire drill

  • Lots of small libraries
  • Deep dependency trees
  • Frequent upstream changes
  • Builds breaking due to solver drift / native deps
  • Pin a complete package universe (compiler + libraries + native deps).
  • Make updates mechanical (not artisanal).
  • Build everything automatically to see what breaks.
  • Treat breakage as data (a matrix), not a surprise.
  • Promote only the green set into the “blessed” environment.

Given the constraints of:

  • reproducibility
  • composability
  • local developer ergonomics
  • production integration
  • long-term maintenance
  • Docker
  • bazel
  • pants

Nix is the only tool I’ve found that makes the full set of tradeoffs explicit and survivable.