Nix Store and Derivations

Prerequisites

  • Nix — the system overview
  • Nix Language — basic syntax (you’ll need to read code examples)

The Nix store

Every build artifact, source tree, and build plan in Nix lives in /nix/store/. A typical store path looks like:

/nix/store/9xf9iqxpzn6ls6r3y6hdfdjqnl0z2g9w-hello-2.12/
/nix/store/6y4iqxpzn....-glibc-2.38-dev/
/nix/store/abc123def....-hello-2.12.drv

The path format is always /nix/store/<hash>-<name>. The hash is computed from all inputs to the build (explained below), making each path content-addressed — analogous to how a git commit SHA encodes the entire tree plus its history.

What goes in it

  • Built package outputs (binaries, libraries, headers, man pages)
  • Source trees copied in before building (from path literals in Nix expressions)
  • .drv files (build plans — see below)
  • Configuration files generated by NixOS

Immutability

Once a store path is written, it is never modified. The Nix daemon sets directory permissions to read-only after building. Only the Nix daemon can write to the store. This means:

  • Two packages built from different inputs will always occupy different paths (different hash)
  • Multiple versions of the same package coexist without conflict
  • Rolling back means switching which store path is active — no destructive overwrite

World-readability and its consequence for secrets

Store paths are readable by all users (permissions r--r--r-- on files). This enables any user to run installed software and the build sandbox to read its dependencies.

Critical consequence: never put secrets in the Nix store. If a password, API token, or private key ends up in a .nix file that gets copied to the store, it will be:

  1. World-readable at /nix/store/<hash>-...
  2. Potentially uploaded to a binary cache
  3. Kept alive as long as any GC root references it

NixOS has dedicated secrets tools (sops-nix, agenix) that decrypt secrets at activation time into /run/ (a tmpfs), never touching the store.

What determines the hash

For standard (input-addressed) derivations, the hash encodes:

  • Package name
  • System string (e.g. x86_64-linux)
  • Builder executable path
  • Build arguments
  • All environment variables passed to the build
  • All input store paths (which are themselves hashed from their inputs, recursively)

The hash is computed before building — purely from the description of how to build. This is what enables binary caches: Nix computes what the output path would be, asks the cache “do you have this hash?”, and downloads it if yes.

# See what's in the store
ls /nix/store/ | head
 
# Check how much space the store uses
du -sh /nix/store/
 
# See a package's runtime dependencies (its closure)
nix-store -qR $(which hello)

Garbage collection

The store uses mark-and-sweep garbage collection. Certain paths are GC roots — anything transitively reachable from a root is kept; everything else can be deleted.

GC roots include:

  • /nix/var/nix/profiles/ — user profiles and NixOS system generations
  • /run/current-system — the running NixOS system
  • Temporary roots held by running Nix processes
# Delete unreachable store paths
nix-collect-garbage
 
# Also delete old profile generations, then GC
nix-collect-garbage -d
 
# See what WOULD be deleted (dry run)
nix-store --gc --print-dead

Derivations

A derivation is a build plan — a precise description of how to produce a store path from inputs:

derivation = {
    inputs:   [store paths of dependencies + sources]
    builder:  [executable to run]
    args:     [arguments to the builder]
    env:      [environment variables]
    outputs:  [store paths to create]
}

When you write pkgs.hello in Nix code, you’re not building hello — you’re working with a derivation value: a data structure describing how hello would be built if requested. The actual building happens later, only if the output doesn’t already exist.

The .drv file

When Nix instantiates a derivation (turns the Nix-language value into something the build system can execute), it writes a .drv file to the store:

# Inspect a derivation
nix show-derivation /nix/store/abc123...-hello-2.12.drv

The .drv contains:

  • outputs — expected output store paths (known before building, computed from input hash)
  • inputSrcs — source files copied from your filesystem
  • inputDrvs — other .drv files this build depends on
  • system — target platform
  • builder — path to the executable that runs the build
  • args — arguments to the builder
  • env — environment variables

The .drv format is intentionally simple and language-agnostic — the build system doesn’t need to understand the Nix language to execute it.

pkgs.hello — what it actually is

pkgs.hello              # a derivation value (NOT a string, NOT a built artifact)
pkgs.hello.name         # → "hello-2.12"
pkgs.hello.version      # → "2.12"
pkgs.hello.outPath      # → "/nix/store/<hash>-hello-2.12"
"${pkgs.hello}"         # → "/nix/store/<hash>-hello-2.12"
                        # (string interpolation coerces to the output path)

The output path is known before building (it’s the hash of the inputs). But the directory at that path doesn’t exist until the derivation is actually built.

Evaluation vs. build — the critical distinction

This is the most important conceptual split in Nix. Almost every misunderstanding traces back to conflating these two phases:

Evaluation is pure computation (no compilers run, no network). Building is sandboxed execution. A binary cache can short-circuit building entirely if the hash matches.

What this means in practice:

  • A typo in your .nix file (servcies.openssh.enable instead of services.openssh.enable) is caught in phase 1 — before any compilation happens. Evaluation errors are cheap and fast.
  • A compiler error inside a package (e.g. a C file that doesn’t compile) is a phase 2 error — it only appears when that derivation is actually built.
  • You can evaluate your entire NixOS config to check for errors without building a single package:
    nix eval .#nixosConfigurations.myhost.config.system.build.toplevel
  • The separation enables aggressive caching: if evaluation produces the same .drv (same hash), the cached build output can be reused without rebuilding.

builtins.derivation vs stdenv.mkDerivation

builtins.derivation is the primitive — a function built into the Nix evaluator:

builtins.derivation {
  name = "hello";
  system = "x86_64-linux";
  builder = "/bin/sh";
  args = [ "-c" "echo 'Hello' > $out" ];
}

This works but gives you nothing: no compiler, no coreutils, no standard build conventions.

stdenv.mkDerivation is a function defined in nixpkgs (not a builtin) that wraps the primitive with a full build environment:

  • bash, coreutils, gcc/clang, binutils, make, grep, sed, awk
  • A phase system: unpackPhasepatchPhaseconfigurePhasebuildPhaseinstallPhasefixupPhase
  • Automatic ./configure && make && make install for autotools packages
  • Automatic ELF binary patching (sets rpath so libraries are found in /nix/store/ instead of /usr/lib/)
stdenv.mkDerivation {
  pname = "hello";
  version = "2.12";
  src = fetchurl {
    url = "mirror://gnu/hello/hello-2.12.tar.gz";
    hash = "sha256-jZkUKv2SV28wsM18tCqNxoCZmLxdYH2Idh9RLibH2yc=";
  };
  # For autotools packages, this is often enough — stdenv handles the rest
}

The build sandbox

Builds execute in isolation to prevent impure dependencies (accidentally using something from the host system that isn’t declared as an input):

What’s restricted:

  • Filesystem: Only explicitly declared inputs are visible (as bind mounts). No /home, /etc, /usr.
  • Network: Disabled by default. Builds must declare all source inputs upfront (via fetchurl, fetchFromGitHub, etc. — these are special “fixed-output derivations” that allow network access but verify the output hash).
  • Time: Fixed to epoch (January 1, 1970) to prevent timestamp-dependent behavior.
  • Users: Runs as unprivileged nixbld users.
  • Environment: Only explicitly passed variables are visible. No $HOME, $USER, no host $PATH.

Implementation on Linux: Uses namespaces (unshare(2)) — mount, PID, network, UTS, IPC — similar to what container runtimes use, but lighter.

This is why Nix builds are reproducible: the sandbox ensures the build can only see what’s declared. If it works in the sandbox, it works anywhere with the same inputs.

See also

  • Nix — the system overview
  • Nix Language — the language that produces derivations
  • nixpkgs — the repository of ~100k derivation definitions
  • Nix Flakes — the modern way to pin inputs and structure projects