Nix Mental Model
Master Nix's functional approach: pure functions, derivations, and the Nix store.
Nix Mental Model
Master the Nix mental model with free flashcards and spaced repetition practice. This lesson covers functional package management, the Nix store structure, and derivation buildingβessential concepts for creating hermetic builds that are reproducible across different environments.
Welcome to the Nix Mental Model π―
Nix is not just another package managerβit's a fundamentally different approach to building and managing software. Understanding Nix requires shifting from imperative thinking ("install this package") to functional thinking ("describe what the system should look like"). This mental model is crucial for mastering hermetic builds, where every input is tracked and every output is reproducible.
Core Concepts π‘
The Nix Store: Immutable Build Artifacts
At the heart of Nix is the Nix store, typically located at /nix/store. Think of it as a content-addressed filesystem where:
- Every package lives in its own isolated directory
- Paths are named with a cryptographic hash of all inputs
- Nothing is ever modified in placeβonly new versions are added
- Multiple versions of the same software coexist peacefully
/nix/store/
βββ a5b8c9d7...-hello-2.10/
β βββ bin/
β β βββ hello
β βββ share/
βββ f3e2d1c0...-python-3.11.2/
β βββ bin/
β β βββ python3
β βββ lib/
βββ 9z8y7x6w...-bash-5.2/
βββ bin/
β βββ bash
βββ share/
Why the hash? The hash at the beginning (like a5b8c9d7...) is computed from:
- Source code inputs
- Build script
- Compiler version
- All dependencies
- Build flags and environment
If ANY of these change, you get a different hash β a different path β a different package. This is how Nix achieves bit-for-bit reproducibility.
π‘ Mental Model Tip: Think of the Nix store like Git for binaries. Each "commit" (build) has a unique hash, and you can have multiple "branches" (versions) simultaneously.
Derivations: Build Recipes as Data π
A derivation is Nix's term for a build recipe. Unlike traditional build scripts that are executed immediately, derivations are:
- Pure data structures describing how to build something
- Lazyβthey don't execute until you explicitly build them
- Composableβderivations can reference other derivations as dependencies
Derivation Structure:
βββββββββββββββββββββββββββββββββββββββ
β Derivation (.drv) β
βββββββββββββββββββββββββββββββββββββββ€
β β’ Name: "myapp-1.0" β
β β’ Builder: /nix/store/.../bash β
β β’ Build script: ./builder.sh β
β β’ Sources: ./src β
β β’ Dependencies: β
β - /nix/store/.../gcc β
β - /nix/store/.../glibc β
β - /nix/store/.../openssl β
β β’ Environment variables β
β β’ Output path: /nix/store/.../... β
βββββββββββββββββββββββββββββββββββββββ
β
β
(Build Time)
β
β
βββββββββββββββββββββββββββββββββββββββ
β Output in /nix/store β
β /nix/store/xyz...-myapp-1.0/ β
βββββββββββββββββββββββββββββββββββββββ
Functional Purity: No Side Effects π
Nix builds are purely functionalβlike mathematical functions:
f(inputs) = output
Same inputs β Same output. Always.
This means during a build:
- β No network access (after sources are fetched)
- β No reading from
/usror/optor system paths - β No accessing the current time or random numbers
- β No writing outside the build directory
- β Only declared inputs are visible
- β Builds run in isolated sandboxes
β οΈ Common Misconception: "Pure" doesn't mean the build process is simpleβit means the process is deterministic and isolated.
The Dependency Graph: DAG All the Way Down π
Nix represents all dependencies as a Directed Acyclic Graph (DAG). Each package is a node, and edges represent "depends on" relationships.
Dependency Graph Example:
βββββββββββββββ
β myapp β
ββββββββ¬βββββββ
β
ββββββ΄βββββ
β β
βββββββββββ ββββββββββββ
β python β β openssl β
ββββββ¬βββββ βββββββ¬βββββ
β β
β β
βββββββββββ ββββββββββββ
β glibc β β zlib β
ββββββ¬βββββ βββββββ¬βββββ
β β
ββββββββ¬βββββββ
β
ββββββββββββ
β gcc β
ββββββββββββ
Key Properties:
- Acyclic: No circular dependencies (A depends on B, B depends on C, C depends on A)
- Explicit: Every dependency must be declared
- Shared: If two packages need
openssl, they use the same/nix/storepath - Parallel: Independent branches can build simultaneously
Lazy Evaluation: Build Only What's Needed β‘
Nix uses lazy evaluation inherited from the Nix expression language:
## This defines a build, but doesn't execute it
let
myPackage = derivation {
name = "myapp";
builder = ./builder.sh;
src = ./src;
};
in
myPackage # Only evaluated when you actually use it
Builds happen on-demand when you:
- Run
nix-build - Install with
nix-env -i - Activate a NixOS configuration
π‘ Mental Model: Nix expressions are like promisesβthey describe what could be built, not what has been built.
Closures: Complete Dependency Sets π¦
A closure is the set of a package plus ALL its runtime dependenciesβeverything needed to run it.
Closure of "myapp": ββββββββββββββββββββββββββββββββββββ β Runtime Closure β ββββββββββββββββββββββββββββββββββββ€ β β’ /nix/store/.../myapp β β β’ /nix/store/.../python β β β’ /nix/store/.../openssl β β β’ /nix/store/.../glibc β β β’ /nix/store/.../zlib β β β’ /nix/store/.../libffi β β β’ /nix/store/.../ncurses β ββββββββββββββββββββββββββββββββββββ Copy this entire set β works anywhere!
Closures are why Nix deployments are so reliable:
## Copy entire closure to another machine
nix-copy-closure --to user@server /nix/store/...-myapp
## Everything needed to run myapp is now there!
Profiles and Generations: Time Travel for Packages β°
Nix doesn't overwrite packagesβit creates generations:
Profile Timeline:
~/.nix-profile β /nix/var/nix/profiles/per-user/alice/profile
|
β
ββββββββββββββββββ΄βββββββββββββββββ
β β
Generation 1 Generation 2 Generation 3 (current)
2024-01-10 2024-01-15 2024-01-20
β β β
β β β
[firefox-120] [firefox-120] [firefox-121]
[python-3.10] [python-3.11] [python-3.11]
[vim-9.0] [vim-9.0] [neovim-0.9]
β
(can rollback to)
Rollback is instantaneous:
## Oops, new package broke something
nix-env --rollback
## Or jump to specific generation
nix-env --switch-generation 2
π§ Mnemonic: Think "Git for your system state"βeach generation is like a commit you can checkout.
Detailed Examples π§
Example 1: Understanding Store Paths
Let's break down a real Nix store path:
/nix/store/4zmn3qw7afk3d8b5h9m2v1l0xjc6nfp8-bash-5.2.15
β β β β
β β β ββ Version
β β ββββββ Package name
ββββββββββββββββββββββββββββββββββββ΄ββββββββ Hash (32 chars)
The hash includes:
{
# Build inputs
src = fetchurl { url = "https://ftp.gnu.org/bash-5.2.15.tar.gz"; };
# Build-time dependencies
buildInputs = [ gcc glibc makeWrapper ];
# Build script
builder = ./builder.sh;
# Configuration flags
configureFlags = [ "--enable-readline" "--with-curses" ];
# System architecture
system = "x86_64-linux";
}
Change ANY of these β different hash β different path β different package.
Example 2: Building a Simple Derivation
Here's how to create a minimal Nix package:
## hello.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation {
pname = "hello-custom";
version = "1.0";
src = ./src; # Local source directory
buildPhase = ''
gcc -o hello hello.c
'';
installPhase = ''
mkdir -p $out/bin
cp hello $out/bin/
'';
}
Build it:
$ nix-build hello.nix
## Output:
## /nix/store/abc123...-hello-custom-1.0
$ ./result/bin/hello
Hello from Nix!
What happened behind the scenes:
- Nix hashed all inputs (source, gcc, build script)
- Created a derivation file (
.drv) - Built in an isolated sandbox at
/tmp/nix-build-... - Moved output to
/nix/store/abc123...-hello-custom-1.0 - Created
resultsymlink pointing there
Example 3: Dependency Management
Let's build a Python application with dependencies:
## myapp.nix
{ pkgs ? import <nixpkgs> {} }:
let
pythonEnv = pkgs.python3.withPackages (ps: [
ps.requests
ps.flask
ps.sqlalchemy
]);
in
pkgs.stdenv.mkDerivation {
pname = "myapp";
version = "2.1.0";
src = ./app;
buildInputs = [ pythonEnv pkgs.postgresql ];
installPhase = ''
mkdir -p $out/bin
cp app.py $out/bin/myapp
# Wrap with correct Python environment
wrapProgram $out/bin/myapp \
--prefix PATH : ${pythonEnv}/bin \
--set PYTHONPATH ${pythonEnv}/${pythonEnv.sitePackages}
'';
nativeBuildInputs = [ pkgs.makeWrapper ];
}
The dependency graph Nix creates:
myapp β pythonEnv β python3 β glibc β gcc
β β
requests postgresql
β
urllib3
β
openssl
Every arrow is a hash-verified, immutable reference!
Example 4: Reproducible Development Environments
One of Nix's killer featuresβpin exact versions:
## shell.nix
let
# Pin nixpkgs to specific commit for reproducibility
nixpkgs = fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/a7ecde854aee5c4c7cd6177f54a99d2c1ff28a31.tar.gz";
sha256 = "162dywda2dvfj1248afxc45kcrg83appjd0nmdb541hl7rnncf02";
};
pkgs = import nixpkgs {};
in
pkgs.mkShell {
buildInputs = [
pkgs.nodejs-18_x
pkgs.yarn
pkgs.postgresql_15
pkgs.redis
];
shellHook = ''
echo "Development environment ready!"
echo "Node: $(node --version)"
echo "PostgreSQL: $(postgres --version)"
export DATABASE_URL="postgresql://localhost/myapp_dev"
'';
}
Enter the environment:
$ nix-shell
## Development environment ready!
## Node: v18.16.0
## PostgreSQL: postgres (PostgreSQL) 15.3
$ which node
/nix/store/xyz...-nodejs-18.16.0/bin/node
$ exit
$ which node
## (node not found - back to your regular system)
π― Real-world benefit: Share shell.nix with your team β everyone gets identical development environments, regardless of their OS or previous installations.
Common Mistakes β οΈ
Mistake 1: Treating Nix Like a Traditional Package Manager
β Wrong thinking: "I'll just nix-env -i everything I need"
β
Right thinking: "I'll declare my environment in a .nix file for reproducibility"
Why: Imperative installs with nix-env -i work, but lose many benefits:
- No version pinning
- No shared configuration with team
- Hard to reproduce on another machine
Use declarative approaches instead:
## configuration.nix or home-manager
environment.systemPackages = [
pkgs.git
pkgs.vim
pkgs.firefox
];
Mistake 2: Expecting Impure Builds to Work
β Wrong: Assuming builds can access /usr/lib or download files
## This will FAIL in Nix sandbox
buildPhase = ''
curl https://example.com/data.json > data.json # No network!
gcc -I/usr/include -L/usr/lib hello.c # No /usr!
'';
β Right: Declare all inputs explicitly
buildInputs = [ pkgs.curl ];
src = fetchurl {
url = "https://example.com/data.json";
sha256 = "..."; # Hash required - fetched BEFORE build
};
Mistake 3: Modifying the Nix Store
β Wrong: Trying to edit files in /nix/store
## This will FAIL - Nix store is read-only
$ vim /nix/store/abc...-myapp/config.txt
## Error: Read-only file system
β Right: Create a new derivation with the changes
## Override existing package
myModifiedApp = pkgs.myapp.overrideAttrs (old: {
postInstall = ''
echo "custom config" > $out/config.txt
'';
});
Mistake 4: Not Understanding Garbage Collection
β Wrong: Deleting /nix/store entries manually
β Right: Let Nix garbage collector manage it
## Remove old generations
nix-collect-garbage --delete-older-than 30d
## Remove ALL unused packages
nix-collect-garbage -d
Nix tracks which store paths are still referenced by:
- Active profiles
- Current NixOS generation
- GC roots in
/nix/var/nix/gcroots/
Mistake 5: Ignoring Binary Caches
β Wrong: Building everything from source
## Slow: rebuilds GCC, glibc, Python, etc.
nix-build '<nixpkgs>' -A python3
β Right: Use cache.nixos.org (default) or set up your own
## nix.conf or configuration.nix
substituters = [
"https://cache.nixos.org"
"https://your-company-cache.example.com"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"your-company-cache:..."
];
Nix will download pre-built binaries when hashes match!
Key Takeaways π
π Quick Reference: Nix Mental Model
| Concept | What It Means |
| Nix Store | Immutable database of all packages at /nix/store |
| Derivation | Pure description of how to build a package |
| Hash | Fingerprint of all build inputs (source + deps + scripts) |
| Closure | Package + complete set of runtime dependencies |
| Profile | User environment with generations for rollback |
| Lazy Evaluation | Builds happen on-demand, not at definition time |
| Purity | No network, no system paths, deterministic output |
| DAG | Dependency graph with no cycles |
Core Principles to Remember:
- π Immutability: Nothing in
/nix/storeever changes - π― Content-Addressed: Hashes ensure reproducibility
- π Functional: Same inputs β same outputs, always
- ποΈ Declarative: Describe what you want, not how to build it
- β‘ Composable: Mix and match packages without conflicts
- π Time-Travel: Rollback to any previous generation
- π Portable: Closures work across machines
π Further Study
Official Resources:
- Nix Pills - A Guided Tour - Deep dive into Nix fundamentals
- NixOS Manual - Nix Store - Technical reference for store mechanics
- Nix Package Manager Guide - Complete language and tooling documentation
π‘ Practice Tip: Start with nix-shell for development environments before diving into writing full derivations. Build your mental model incrementally!