You are viewing a preview of this lesson. Sign in to start learning
Back to Hermetic Builds

Derivations and Nix Store

Learn how Nix treats builds as pure functions with inputs producing outputs in /nix/store.

Derivations and Nix Store

Understand how Nix builds software reproducibly with free flashcards and spaced repetition practice. This lesson covers derivations (Nix's build instructions), the Nix store (immutable package storage), and content-addressable pathsβ€”essential concepts for mastering hermetic builds and the Nix mental model.

Welcome to the Nix Mental Model πŸ’»

If you've ever wondered how Nix achieves perfect reproducibility where builds work identically on any machine, the answer lies in two fundamental concepts: derivations and the Nix store. These aren't just technical detailsβ€”they're the philosophical foundation that makes Nix different from traditional package managers.

Think of a derivation as a recipe card that specifies exactly how to build something, including every ingredient (dependency) and every step. The Nix store is like a giant warehouse where the results of these recipes are stored in individually labeled boxes that can never be modified. Once something goes in the store, it's frozen in time forever.

🎯 Why This Matters: Understanding derivations and the Nix store is critical for:

  • Debugging build failures
  • Creating your own packages
  • Understanding why Nix guarantees reproducibility
  • Working with Nix in production environments

Core Concept 1: The Nix Store πŸ“¦

What is the Nix Store?

The Nix store is a directory (typically /nix/store) where Nix keeps all built packages, libraries, and artifacts. Every item in the store has a unique path based on a cryptographic hash of its inputs.

Structure of a Store Path:

/nix/store/b6gvzjyb2pg0kjfwrjmg1vfhh54ad73z-firefox-107.0
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  hash                   name
  • Hash (32 characters): Derived from all build inputs (source code, dependencies, compiler flags, build script)
  • Name: Human-readable identifier (package-version)

Key Properties of the Nix Store πŸ”’

  1. Immutability: Once a path exists in /nix/store, it can never be modified. If you change anything about a build, you get a different hash and a different path.

  2. Content-Addressable: The hash acts like a fingerprint. If two builds have identical inputs, they produce the same hash and can share the same store path.

  3. Isolation: Each package version lives in its own directory. You can have python-3.9, python-3.10, and python-3.11 all installed simultaneously without conflicts.

  4. Garbage Collection: Store paths are only kept if something references them. Unreferenced paths can be cleaned up safely.

NIX STORE MENTAL MODEL

/nix/store/
  β”‚
  β”œβ”€ abc123...xyz-glibc-2.35/
  β”‚   β”œβ”€ lib/
  β”‚   └─ include/
  β”‚
  β”œβ”€ def456...uvw-openssl-3.0/
  β”‚   β”œβ”€ lib/
  β”‚   β”‚   β”œβ”€ libssl.so β†’ (immutable)
  β”‚   β”‚   └─ libcrypto.so
  β”‚   └─ bin/
  β”‚
  └─ ghi789...rst-nodejs-18.12/
      β”œβ”€ bin/node
      └─ lib/

  Each path is ISOLATED
  Each path is IMMUTABLE βœ“
  Hash changes = new path βœ“

πŸ’‘ Tip: You can explore your store with ls /nix/store | head to see actual store paths on your system.

Why Hashes Matter πŸ”

The hash isn't randomβ€”it's computed from:

  • Source code (or download URL + checksum)
  • All dependencies (their store paths)
  • Build script content
  • Environment variables used during build
  • Compiler and tool versions

Change any of these? You get a different hash, a different path, and a completely separate build.

Example Scenario:

You build myapp that depends on openssl-1.1. Later, you update to openssl-3.0. Your new build gets a new hash because the dependency changed:

/nix/store/old-hash-myapp-1.0/  (depends on openssl-1.1)
/nix/store/new-hash-myapp-1.0/  (depends on openssl-3.0)

Both versions can coexist! The old one doesn't disappear until garbage collected.


Core Concept 2: Derivations (.drv files) πŸ“

What is a Derivation?

A derivation is Nix's blueprint for building something. It's a specification that describes:

  • What to build (source code or inputs)
  • How to build it (build commands)
  • What dependencies are needed
  • What environment variables to set

Derivations are stored as .drv files in the Nix store before they're built.

Conceptual Structure:

Derivation ComponentPurposeExample
outputsWhat gets produced"out", "dev", "doc"
inputDrvsDependencies (other derivations)gcc, make, glibc
inputSrcsSource filestarball, patches
platformTarget systemx86_64-linux
builderProgram that executes build/nix/store/...-bash/bin/bash
argsArguments to builder["-e", "build-script.sh"]
envEnvironment variablesCC=gcc, PREFIX=$out

Derivation Lifecycle πŸ”„

DERIVATION WORKFLOW

1. Write Nix Expression          2. Instantiate
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ default.nix β”‚  nix-instantiate  β”‚  .drv file   β”‚
   β”‚             β”‚  ─────────────→   β”‚              β”‚
   β”‚ { stdenv,   β”‚                   β”‚ /nix/store/  β”‚
   β”‚   fetchurl, β”‚                   β”‚ abc...xyz-   β”‚
   β”‚   ... }     β”‚                   β”‚ myapp.drv    β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                                             β”‚
                                             β”‚ nix-store --realise
                                             ↓
                              3. Build (realize)
                                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                 β”‚ Built output β”‚
                                 β”‚              β”‚
                                 β”‚ /nix/store/  β”‚
                                 β”‚ xyz...abc-   β”‚
                                 β”‚ myapp-1.0/   β”‚
                                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step-by-step:

  1. Nix Expression β†’ You write high-level Nix code (like default.nix)
  2. Instantiation β†’ Nix evaluates your expression and creates a .drv file
  3. Realization β†’ Nix executes the .drv instructions to produce the actual build output

πŸ’‘ Key Insight: The .drv file is itself stored in /nix/store and has its own hash. It's the "recipe", while the final output is the "meal".


Core Concept 3: How Hashing Works πŸ”’

Computing Store Path Hashes

Nix uses a cryptographic hash function (SHA-256, truncated to 160 bits, base-32 encoded) to compute store paths. The hash input includes:

hash_input = (
  "output:out",
  "source" or derivation_path,
  all_dependency_paths,
  build_system,
  derivation_name
)

store_path = "/nix/store/" + base32(sha256(hash_input))[0:32] + "-" + name

Fixed-Output Derivations vs Regular Derivations

Regular Derivation:

  • Hash computed from inputs (source, dependencies, build script)
  • Two developers building the same inputs get the same hash before building
  • Used for most software builds

Fixed-Output Derivation:

  • Hash computed from expected output (usually a checksum)
  • Used for downloading files from the internet
  • Allows network access during build (normally prohibited)

Example: Fetching a tarball

fetchurl {
  url = "https://example.com/foo-1.0.tar.gz";
  sha256 = "0abc...xyz";  # Expected output hash
}

Nix knows the output hash before downloading. If the downloaded file's hash doesn't match, the build fails. This is how Nix ensures reproducibility even for network resources.

HASH COMPUTATION COMPARISON

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚        REGULAR DERIVATION                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Input Hash = f(source, deps, script)        β”‚
β”‚                    ↓                         β”‚
β”‚             Build Process                    β”‚
β”‚                    ↓                         β”‚
β”‚         Output (hash unknown until built)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      FIXED-OUTPUT DERIVATION                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Output Hash = sha256 (declared upfront)     β”‚
β”‚                    ↓                         β”‚
β”‚       Download/Build (network allowed)       β”‚
β”‚                    ↓                         β”‚
β”‚  Verify: actual_hash == declared_hash βœ“      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Example 1: Simple Derivation πŸ› οΈ

Let's build a minimal "Hello World" program using a derivation.

File: hello.nix

{ pkgs ? import <nixpkgs> {} }:

pkgs.stdenv.mkDerivation {
  name = "hello-custom";
  
  src = pkgs.writeText "hello.c" ''
    #include <stdio.h>
    int main() {
      printf("Hello from Nix!\n");
      return 0;
    }
  '';
  
  buildInputs = [ pkgs.gcc ];
  
  unpackPhase = "true";  # No archive to unpack
  
  buildPhase = ''
    gcc ${pkgs.writeText "hello.c" ''#include <stdio.h>
    int main() { printf("Hello from Nix!\n"); return 0; }''} -o hello
  '';
  
  installPhase = ''
    mkdir -p $out/bin
    cp hello $out/bin/
  '';
}

Build it:

nix-build hello.nix
## Output: /nix/store/xyz123...-hello-custom

./result/bin/hello
## Output: Hello from Nix!

What happened:

  1. Nix evaluated hello.nix and created a .drv file
  2. The .drv specifies: gcc as dependency, build commands, install location
  3. Nix executed the build in an isolated environment
  4. Output was placed in /nix/store/xyz123...-hello-custom/bin/hello
  5. A symlink result points to the store path

πŸ’‘ Note: $out is a special variable that points to the store path where output should go.


Example 2: Inspecting a Derivation πŸ”

Let's peek inside an actual .drv file.

## Instantiate without building
nix-instantiate hello.nix
## Output: /nix/store/abc456...-hello-custom.drv

## Read the derivation
nix show-derivation /nix/store/abc456...-hello-custom.drv

Output (simplified JSON):

{
  "/nix/store/abc456...-hello-custom.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/xyz123...-hello-custom"
      }
    },
    "inputDrvs": {
      "/nix/store/def789...-gcc-11.3.0.drv": ["out"],
      "/nix/store/ghi012...-bash-5.1.drv": ["out"]
    },
    "inputSrcs": [
      "/nix/store/jkl345...-hello.c"
    ],
    "platform": "x86_64-linux",
    "builder": "/nix/store/ghi012...-bash-5.1/bin/bash",
    "args": ["-e", "/nix/store/mno678...-builder.sh"],
    "env": {
      "buildInputs": "/nix/store/def789...-gcc-11.3.0",
      "name": "hello-custom",
      "out": "/nix/store/xyz123...-hello-custom",
      "system": "x86_64-linux"
    }
  }
}

Key observations:

  • The output path is predetermined (based on the hash)
  • All dependencies are referenced by their full store paths
  • The builder is bash (also from the store)
  • Environment variables are explicitly listed

πŸ”§ Try this: Run nix show-derivation $(nix-instantiate '<nixpkgs>' -A hello) to see a real-world package derivation.


Example 3: Understanding Dependencies πŸ”—

Derivations form a dependency graph. Let's visualize this.

Scenario: Building a simple web app

{ pkgs ? import <nixpkgs> {} }:

let
  myNodeApp = pkgs.stdenv.mkDerivation {
    name = "my-node-app";
    src = ./src;
    buildInputs = [ pkgs.nodejs pkgs.yarn ];
    buildPhase = "yarn install && yarn build";
    installPhase = "cp -r dist $out/";
  };
in
  myNodeApp

Dependency graph:

DEPENDENCY GRAPH

     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚  my-node-app.drv    β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
       ↓                 ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ nodejs.drv  β”‚   β”‚  yarn.drv   β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚                 β”‚
   β”Œβ”€β”€β”€β”΄β”€β”€β”€β”         β”Œβ”€β”€β”€β”΄β”€β”€β”€β”
   ↓       ↓         ↓       ↓
β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”
β”‚ gcc β”‚ β”‚ icu β”‚  β”‚node β”‚ β”‚ ... β”‚
β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜

All dependencies are EXPLICIT
All are referenced by store paths
β†’ Perfect reproducibility

What makes this hermetic:

  1. No implicit dependencies: Can't accidentally use system libraries
  2. Exact versions: Not "nodejs" but "/nix/store/xyz-nodejs-18.12.0"
  3. Transitive closure: All dependencies of dependencies are tracked
  4. Reproducible: Same source + same dependencies = same hash = same build

πŸ’‘ Mnemonic: Think "Derivations Define Dependencies Deterministically" (4 D's)


Example 4: Multiple Outputs πŸ“€

Derivations can produce multiple outputs (binaries, libraries, documentation, etc.).

{ pkgs ? import <nixpkgs> {} }:

pkgs.stdenv.mkDerivation {
  name = "multi-output-example";
  
  outputs = [ "out" "dev" "doc" ];
  
  src = ./src;
  
  buildPhase = ''
    # Compile library
    gcc -shared -fPIC mylib.c -o libmylib.so
    
    # Create headers
    cp mylib.h mylib-headers/
    
    # Generate docs
    echo "Documentation" > README
  '';
  
  installPhase = ''
    # Binaries go to $out
    mkdir -p $out/lib
    cp libmylib.so $out/lib/
    
    # Development headers go to $dev
    mkdir -p $dev/include
    cp mylib.h $dev/include/
    
    # Documentation goes to $doc
    mkdir -p $doc/share/doc
    cp README $doc/share/doc/
  '';
}

Result in store:

/nix/store/aaa111...-multi-output-example/       # $out (runtime)
/nix/store/bbb222...-multi-output-example-dev/   # $dev (headers)
/nix/store/ccc333...-multi-output-example-doc/   # $doc (docs)

Why multiple outputs?

  • Space efficiency: Don't need docs/headers in production deployments
  • Dependency precision: Dev tools depend on $dev, users depend on $out
  • Closure size reduction: Smaller Docker images, faster downloads

πŸ€” Did you know? The glibc package in nixpkgs has 9 different outputs (out, bin, dev, static, debug, ...) to optimize for different use cases!


Common Mistakes ⚠️

Mistake 1: Expecting Store Paths to Be Readable πŸ‘€

Wrong assumption:

ls /nix/store/firefox
## Error: No such file or directory

Why it fails: The directory name includes a hash. You can't predict the full path.

Correct approach:

## Let Nix tell you the path
nix-build '<nixpkgs>' -A firefox

## Or use nix-env to query
nix-env -qa --installed firefox

Mistake 2: Trying to Modify Store Paths 🚫

Wrong:

chmod +w /nix/store/xyz-myapp/bin/app
echo "hacked" >> /nix/store/xyz-myapp/bin/app
## Permission denied (store is read-only)

Why it fails: The Nix store is mounted read-only for non-root users (or protected by permissions).

Correct approach: If you need to modify something, create a new derivation that wraps or patches the original:

pkgs.runCommand "myapp-modified" {} ''
  mkdir -p $out/bin
  cp ${originalApp}/bin/app $out/bin/app
  echo "# My modifications" >> $out/bin/app
''

Mistake 3: Forgetting Impure Dependencies 🌐

Wrong:

stdenv.mkDerivation {
  name = "bad-example";
  buildPhase = ''
    # This will fail! No network access during build
    curl https://example.com/data.json > data.json
    gcc main.c -o app
  '';
}

Why it fails: Regular derivations run in a sandbox with no network access.

Correct approach: Use fetchurl or fetchFromGitHub (fixed-output derivations) to get external resources before the build:

let
  dataFile = pkgs.fetchurl {
    url = "https://example.com/data.json";
    sha256 = "0abc123...";  # Declare expected hash
  };
in
stdenv.mkDerivation {
  name = "good-example";
  buildPhase = ''
    cp ${dataFile} data.json
    gcc main.c -o app
  '';
}

Mistake 4: Not Understanding Hash Changes πŸ”„

Confusion: "I only changed a comment in my source code, why did the entire dependency tree rebuild?"

Explanation: Any change to inputs changes the hash, which changes the store path, which means all dependent packages see a different path and need rebuilding.

CHANGE PROPAGATION

Before:                After (comment added):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   app-v1    β”‚        β”‚   app-v2    β”‚
β”‚  /nix/...   β”‚        β”‚  /nix/...   β”‚ ← New hash!
β”‚   abc123    β”‚        β”‚   xyz789    β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚                      β”‚
       ↓                      ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   lib-v1    β”‚        β”‚   lib-v1    β”‚ ← Same, but...
β”‚  /nix/...   β”‚        β”‚  /nix/...   β”‚
β”‚   def456    β”‚        β”‚   def456    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  Dependents of app-v1        Dependents of app-v2
  reference abc123            must reference xyz789
       ↓                            ↓
  EVERYTHING rebuilds that depends on changed path

Mitigation: Use Nix's binary cache (like cache.nixos.org) so you download pre-built binaries instead of rebuilding.


Key Takeaways 🎯

πŸ“‹ Quick Reference Card

Concept Definition Key Property
Nix Store Directory holding all packages Immutable, content-addressed
Store Path /nix/store/hash-name Hash from all inputs
Derivation Build instructions (.drv) Declarative, deterministic
$out Output path variable Where build results go
Fixed-output Hash of output (not inputs) Network access allowed
Sandbox Isolated build environment No network, limited filesystem

Core Principles:

βœ… Immutability: Store paths never change
βœ… Reproducibility: Same inputs β†’ same outputs
βœ… Isolation: No interference between builds
βœ… Explicit: All dependencies declared
βœ… Deterministic: Hash determines everything


Mental Models 🧠

The Restaurant Analogy:

  • Nix Expression = Menu item description ("Caesar Salad with dressing")
  • Derivation (.drv) = Recipe card (exact ingredients, steps, tools)
  • Nix Store = Walk-in freezer with labeled containers
  • Store Path = Container label (includes date, source, chef ID)
  • Building = Following the recipe to make the dish
  • Result = Finished meal in a sealed container with hash label

The Version Control Analogy:

  • Store Path Hash = Git commit SHA
  • Derivation = Commit metadata (author, date, message, parent commits)
  • Nix Store = Git object database (.git/objects)
  • Building = Checking out a commit
  • Dependencies = Parent commits in Git history

Further Study πŸ“š

  1. Nix Pills - Chapter 18 (Nix Store Paths): Deep dive into how Nix computes store path hashes
    πŸ”— https://nixos.org/guides/nix-pills/nix-store-paths.html

  2. Nix Manual - Store Path Specification: Official technical specification for store path computation
    πŸ”— https://nixos.org/manual/nix/stable/store/store-path.html

  3. Eelco Dolstra's PhD Thesis: The foundational research behind Nix (Chapter 4 covers the Nix store model)
    πŸ”— https://edolstra.github.io/pubs/phd-thesis.pdf


You now understand the foundation of Nix's hermetic build system! πŸŽ‰ The store and derivations are the "secret sauce" that makes reproducible builds possible. Everything else in Nix builds on these concepts. Next, explore how to write your own derivations and leverage the massive nixpkgs ecosystem.