Reproducibility: Same inputs = same hash.
Caching: Already built? Reuse it!
Isolation: Different versions coexist peacefully.
Atomic upgrades: New hash = new path.
Once built, never changes.
Upgrades create new paths.
Old versions remain until garbage collected.
Rollbacks are just switching symlinks.
/nix/store/iki3g1iyxydm65k7hm0r3ssm8l6mvlb6-python3-3.12.8
/nix/store/8bwmgvfcyys3kfia055ih7gask3fid7s-python3-3.14.2
Both exist simultaneously!
Your programs use whatever version they need.
$ nix-env --install firefox
...
$ nix-env --rollback # Instant undo!
Update symbolic link at /nix/var/nix/profiles/default path.
During a Nix build:
Block network access in most cases.
Block access to /usr, /bin, etc.
Prevent environment variable leakage.
Allow only declared dependencies.
Result: Reproducible builds!
$ nix-store --query --references /nix/store/...-hello
/nix/store/...-glibc-2.35
/nix/store/...-gcc-11.3.0-lib
$ nix-store --query --referrers /nix/store/...-glibc
/nix/store/...-hello
/nix/store/...-bash
/nix/store/...-coreutils
A package with all its dependencies, resolved recursively.
$ nix-store --query --requisites /nix/store/...-hello
/nix/store/...-hello
/nix/store/...-glibc-2.35
/nix/store/...-gcc-11.3.0-lib
/nix/store/...-linux-headers-5.19
This is everything needed to run the program.
The LEGO set that completely built.
$ nix path-info --closure-size --human-readable nixpkgs#hello
/nix/store/...-hello-2.12.2 31.8 MiB
Why does "Hello World" need 31.8 MiB?
glibc-2.42-47: 29.0 MiB
...
hello-2.12.2: 268.2 KiB
nix-collect-garbage --delete-older-than 30d
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
outputs = { self, nixpkgs }: {
packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.hello;
};
}
Explicit inputs with locked versions.
Better reproducibility.
Easier to share and compose.
{
description = "Show UTC Date & Time";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs }:
let
forAllSystems = f:
nixpkgs.lib.genAttrs
[ "x86_64-linux" ... "aarch64-darwin" ]
(system: f nixpkgs.legacyPackages.${system});
in
{ packages = forAllSystems (pkgs: {
default = pkgs.runCommand "show-utc-datetime" { ... } ''...'';
});
};
}
description = "Show UTC Date & Time";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
Declare external dependencies with exact sources.
Create a flake.lock to pin the exact revision.
Order from which LEGO® catalog edition.
outputs = { self, nixpkgs }:
let
forAllSystems = f:
nixpkgs.lib.genAttrs
[ "x86_64-linux" "aarch64-linux"
"x86_64-darwin" "aarch64-darwin" ]
(system: f nixpkgs.legacyPackages.${system});
in
The outputs is a function receiving resolved inputs.
{
packages = forAllSystems (pkgs: {
default =
pkgs.runCommand "show-utc-datetime"
# ...
});
};
The build logic is identical to our regular expression.
nix build
nix flake show
nix flake metadata
nix run
Everything is still derivations under the hood!
nix why-depends .#default \
"$(nix path-info --recursive .#default | grep bash)"
# /nix/store/42c3md0x...-show-utc-datetime
# └───/nix/store/vlfjhc97...-bash-5.3p9
nix path-info --recursive .#default
nix path-info --closure-size .#default
Explore your dependency graph.
Development: Consistent, reproducible environments.
CI/CD: Hermetic, cacheable builds.
Production: Atomic deployments. Declarative configuration. SBOM.
Multi-user: Isolated user environments. No dependency conflicts.
Benefits:
Extreme reliability.
Perfect reproducibility.
Drawbacks:
Disk space from storing many versions.
Steep learning curve.
Expressions ≈ Design Sketches
Derivations ≈ Instruction Manuals
Hashes ≈ Set Numbers
Explore your own /nix/store.
Read some .drv files.
Trace dependencies with why-depends.
Write your first Nix expression.
Join the Nix community.
sheeeng.github.io/slides
Ever looked inside /nix/store and felt immediate confusion? You aren't alone. For many, the "magic" of Nix is hidden behind cryptic hashes and the mysterious "derivation." This talk strips away the jargon to explain how Nix actually works using a simple metaphor: a giant, immutable LEGO set. We'll explore how Nix builds software in total isolation, why your system can't "break" like traditional distros, and how every package is just a recipe waiting to be snapped into place. "LEGO® is a trademark of the LEGO Group of companies which does not sponsor, authorize or endorse this site/presentation."
Start with something familiar but confusing. These cryptic paths are what many users see first and immediately feel lost. The hashes look random, the structure seems opaque.
These are the common questions newcomers have. Traditional package managers hide complexity behind familiar directories like /usr/bin. Nix does the opposite. It shows you everything, but at first glance it seems like chaos.
We'll go from confusion to clarity using a simple metaphor that makes Nix's complexity make sense.
Now we introduce the key metaphor that will help everything make sense.
Conventional package managers are like throwing all your LEGO bricks into one big bin. Everything mixed together: packages share directories like /usr/lib, so two versions of the same library compete for one path. Nix gives every package a unique hash-based path in /nix/store. Upgrading might break: replacing a shared dependency can silently break other packages. Nix builds in isolation, so upgrading one package never touches another's files. Hard to undo changes: upgrades overwrite files in place with no previous state to restore. Nix tracks generations, and every old package remains in the store for instant rollback. Dependency hell: conflicting version requirements become unresolvable when only one global version is allowed. Nix lets each package depend on its own specific versions at distinct store paths.
This is the core metaphor. Each LEGO set is self-contained, has clear instructions, and has a unique identifier. Sound familiar? Manual: https://nix.dev/manual/nix/2.34/store/store-object/content-address
The immutability of LEGO sets maps perfectly to Nix's immutable store. Once built, you don't change them; you build new ones. The Kragle holds them together!
Now let's understand the relationship between Nix expressions and derivations.
A Nix expression is just declarative source code written in the Nix language. You describe the inputs, the build steps, and the expected outputs. Nix expressions are pure functions, meaning the same inputs always produce the same result. This is the file you edit as a developer. Glossary: https://nix.dev/manual/nix/2.34/glossary#gloss-nix-expression
This is the most important distinction in Nix. You write expressions, which is human-friendly source code. Then, Nix turns these expressions into full resolved, machine-readable build recipe, which is called derivations.
A derivation is just a recipe. It tells Nix exactly what inputs are needed, what steps to perform, and what the final output should look like. Glossary: https://nix.dev/manual/nix/2.34/glossary#gloss-derivation
Glossary: https://nix.dev/manual/nix/2.34/glossary#gloss-derivation
This is a real Nix expression you can build yourself. Don't worry if it looks dense. We will walk through it piece by piece over the next few slides.
Every Nix file is a function. The `?` operator is a 'default value if null' operator, if you don't find a good way to fit it on the slide? This line says "give Nix a package set, or Nix will load nixpkgs itself." This is how the expression knows where to find its dependencies. It is the shopping list before you start building.
The runCommand is the simplest way to create a derivation. You give it a name, declare what tools you need at build time, and provide a build script. The name you choose here is what appears after the hash in the final store path. The `"show-utc-datetime"` becomes the package name in the store path. The `nativeBuildInputs` lists dependencies available during the build. NOTE: An important note, is to notice that a nix-built bash-script, which contains e.g. the output of $(date) or `cat /dev/urandom` in the script-file, would never be possible to make reproducible. Thus, nix is not a guarantee for reproducibility, but can be thought of as a good system of guardrails to build software as reproducible as possible. - Hat tip to Christian Chavez!
This is the actual build logic. Notice how $out is a placeholder for the final store path that Nix will compute. The script uses absolute paths to dependencies, pointing directly into the Nix store. No reliance on PATH or system-wide binaries. That is total isolation in action.
Nix replaces the string interpolation expressions with absolute paths into the store. The resulting script does not depend on any system PATH. It points directly at a specific version of bash and a specific version of uutils-coreutils-noprefix. If either dependency changes, the hash changes, and you get a completely new store path. Glossary: https://nix.dev/manual/nix/2.34/glossary#gloss-string-interpolation Manual: https://nix.dev/manual/nix/2.34/language/string-interpolation
First, evaluate the expression to get the derivation. Next, we execute a derivation (building it in the sandbox) to produce its outputs. When we run "nix build", we are producing (or more precisely, realising) the derivation by ensure the store paths are valid. The hash in the output path is deterministic. If we build this on another machine with the same nixpkgs version, we get the exact same hash. That is reproducibility. Glossary: https://nix.dev/manual/nix/2.34/glossary#gloss-realise Ensure a store path is valid. This can be achieved by: - Fetching a pre-built store object from a substituter. - Building the corresponding store derivation. - Delegating to a remote machine and retrieving the outputs. Glossary: https://nix.dev/manual/nix/2.34/glossary#gloss-derivation Derivations are implemented as operating system processes that run in a sandbox. This sandbox by default only allows reading from store objects specified as inputs, and only allows writing to designated outputs to be captured as store objects.
The first thing Nix reports is which derivation it plans to build. The .drv file is the real derivation. Our .nix file was just the expression that produced it. The content-addressed hash encodes every input: the build script, dependencies, system architecture, everything. Glossary: https://nix.dev/manual/nix/2.34/glossary#gloss-content-address
Nix checks the binary cache before building anything. Since someone already built uutils-coreutils with the exact same hash, Nix downloads the pre-built result. This saves enormous amounts of time. Without the cache, it would need to compile the entire Rust codebase for uutils-coreutils from source.
Now Nix actually executes the derivation. It runs our build script in total isolation. The only tools available are what we declared in nativeBuildInputs variable. This is why Nix builds are reproducible. Nothing from the host system can leak in in the build process.
If we use nix-output-monitor tool, it gives us a clear picture of what happened. nix build --rebuild --log-format internal-json --verbose --file show-utc-datetime.nix |& nix run nixpkgs#nix-output-monitor -- --json The dependency graph shows that show-utc-datetime was built locally. All dependencies including uutils-coreutils were already available in the store, so nothing needed to be downloaded.
The .drv file is what Nix actually executes. It contains the fully resolved build recipe with all store paths filled in. The outputs section tells you exactly where the build result will land. Both the derivation hash and the output hash are deterministic.
Even the shell that runs the build is a specific, immutable store path. Nix does not use /bin/bash from the host. It uses its own bash, locked to a specific version. The arguments point to stdenv setup scripts that configure the build environment before running our build command.
Input derivations form the dependency graph. Each one must be available in the store before our build can start. Notice that stdenv is also an input even though we did not declare it explicitly. The runCommand helper added it for us. This is how Nix ensures complete dependency tracking.
This is the fully expanded version of our build script. Compare this to the original Nix expression. Every Nix interpolation has been replaced with a concrete store path. This is the script that Nix actually executes inside the sandbox. The system field tells Nix which platform this derivation targets.
The output is a clean directory with a single executable. Looking inside the script, you can see that every path is an absolute store path. There is no ambiguity about which bash or which date command runs. This is what makes Nix packages self-contained.
The result symlink is a convenience. It points at the immutable store path. You can copy the entire closure to another machine, and it will produce the same output. No "works on my machine" surprises. Output a UTC timestamp in ISO 8601 compact format. Run the script on any machine with this Nix closure.
This is the build process in a nutshell. The Nix expression is evaluated to produce a derivation. The derivation is then executed in a sandbox to produce the output. Everything is deterministic and traceable.
Think of a store path as an opaque, unique identifier: The only way to obtain store path is by adding or building store objects. A store path will always reference exactly one store object. Store paths are pairs of: - A 20-byte / 32 ASCII characters digest for identification. - A symbolic name for people to read. The hash isn't random. It's deterministic! It's computed from all the inputs, the build recipe, and even the compiler version. Change anything, and you get a different hash. Manual: Store Path: https://nix.dev/manual/nix/2.34/store/store-path Manual: Complete Store Path Calculation: https://nix.dev/manual/nix/2.34/protocols/store-path
The store directory defaults to /nix/store, but is in principle arbitrary. A store path is rendered to a file system path as the concatenation of: -Store directory (typically /nix/store) -Path separator (/) - Digest rendered in Nix32, a variant of base-32 (20 -hash bytes become 32 ASCII characters) -Hyphen (-) -Name Manual: Store Path: https://nix.dev/manual/nix/2.34/store/store-path Manual: Complete Store Path Calculation: https://nix.dev/manual/nix/2.34/protocols/store-path Manual: Nix32 Encoding: https://nix.dev/manual/nix/2.34/protocols/nix32
Hashes are the secret sauce. They enable all of Nix's superpowers: reproducibility, caching, isolation, and safe upgrades.
This is why Nix systems are so stable. You're never modifying existing working software. Instead, you're always creating new versions alongside the old ones.
Multiple versions coexist peacefully. Program A can use Python 3.12 while Program B uses Python 3.14. No conflicts. tree -L 1 /nix/store/ | grep 'python3-3.12' rg --files --follow --glob "**/bin/python" /nix/store
Operations are atomic. Either they complete fully or they don't happen at all. And rolling back is just changing which generation your profile points to. Glossary: https://nix.dev/manual/nix/2.34/glossary#gloss-profile A symlink to the current user environment of a user, e.g., /nix/var/nix/profiles/default.
This is crucial. Nix builds happen in a sandbox where the only things available are what you explicitly declared. No hidden dependencies, no accidental reliance on system packages. Every build happens in a clean room. Nix sets up the environment from scratch every time, ensuring consistency. You can verify this yourself: nix-build '<nixpkgs>' --attr hello --check --no-out-link --no-substitute
You can explore the dependency graph to understand what each package needs. What does this package reference? What references this package? # Run on macOS system. nix-store --query --references "$(nix-build '<nixpkgs>' --attr hello --no-out-link)" /nix/store/...5rjcqb2-libiconv-109.100.2 # TODO: Run on Linux system.
The closure is the complete set of everything needed. If you copy a closure to another machine, the program will work because all dependencies are included.
This often surprises people. Even a simple program has dependencies. But remember, glibc is shared across many programs in the store. Get total closure size, the sum of the package and all its transitive dependencies: $ nix path-info --closure-size --human-readable nixpkgs#hello /nix/store/...rnzdv4a-hello-2.12.2 31.8 MiB Get cumulative closure size per package, each entry includes the sizes of its own dependencies: $ nix path-info --recursive --closure-size --human-readable nixpkgs#hello /nix/store/...miph269-libunistring-1.4.1 2.0 MiB /nix/store/...h5q82z8-libidn2-2.3.8 2.3 MiB /nix/store/...zzmwkxj-xgcc-15.2.0-libgcc 193.1 KiB /nix/store/...4bayxw4-glibc-2.42-47 31.5 MiB /nix/store/...rnzdv4a-hello-2.12.2 31.8 MiB Get individual Nix ARchive (NAR) size per package, how much disk space each single store path occupies: $ nix path-info --recursive --size --human-readable nixpkgs#hello /nix/store/...miph269-libunistring-1.4.1 2.0 MiB /nix/store/...h5q82z8-libidn2-2.3.8 359.6 KiB /nix/store/...zzmwkxj-xgcc-15.2.0-libgcc 193.1 KiB /nix/store/...4bayxw4-glibc-2.42-47 29.0 MiB /nix/store/...rnzdv4a-hello-2.12.2 268.2 KiB
Garbage collection is safe because Nix knows exactly what's in use and what's not. It traces from roots like your active profiles and running programs, then only deletes what's unreachable. 1. Find all roots, such as active profiles. 2. Trace all references from roots. 3. Delete unreachable store paths. 4. Reclaim disk space.
Let's look at modern Nix with flakes and wrap up with why all of this matters.
Flakes are the modern way to work with Nix. They make dependencies explicit, lock versions automatically, and make projects easier to share.
This is the complete flake version of our show-utc-datetime example. We will walk through it piece by piece.
Unlike the standalone expression that used import <nixpkgs>, a flake pins its inputs to a specific Git revision. The lock file ensures everyone building this flake uses the exact same nixpkgs commit. No more "it works on my machine" because of different channels.
The outputs function receives the resolved inputs. The forAllSystems helper generates packages for all four common platforms: x86 Linux, ARM Linux, x86 macOS, and ARM macOS. This means the same flake works everywhere without modification, provided the binary are supported on those platforms.
The package definition inside the flake is the same runCommand we used before. The only difference is that it is wrapped in the flake structure. The default attribute means you can build it with just nix build without specifying a package name.
Flakes are just a nicer interface. Under the hood, it's still the same derivation-based system we've been discussing. The `nix build` evaluates the default package from our flake.nix and produces a derivation. The implicit defaults expanded: nix build = nix build .#default nix flake show = nix flake show . nix flake metadata = nix flake metadata . nix run = nix run .#default
Same analysis tools work with flakes. Using .#default references the default package from your flake.nix directly. You can trace dependencies, understand closure sizes, and optimize your builds.
This architecture enables all of Nix's benefits. It's not magic. It's careful engineering. These benefits translate to real productivity gains in software development and operations. Development: Nix dev shells activate instantly and share dependencies across projects via the Nix store. No extra files to maintain and no caching surprises. CI/CD: Nix builds are hermetic at the package level, meaning they are insensitive to the libraries and other software installed on the build machine. Every dependency is pinned by hash. Binary caches save time by reusing previously built outputs. Production: Nix can produce minimal container images containing only the exact closure needed. Nix also complements container workflows by generating optimized images from precise dependency graphs. Rollbacks are trivial because old versions remain in the store. SBOM: Because Nix tracks the full dependency graph, generating a complete software bill of materials is trivial. Multi-user: Nix allows multiple users on a single machine to have isolated package sets. Just symlinks to immutable store paths, with no additional overhead.
Nothing is perfect. Nix trades disk space for reliability, and has a learning curve. But for some, the benefits far outweigh the costs.
These are the core concepts. Master these, and Nix will start to make sense.
The best way to learn is by doing. Start exploring your own system and see how things connect.
[15 seconds] Thanks! Slides are on GitHub. Questions? If you have any questions, feel free to find me after the talk or reach out online. Happy Nixing!
LEGO® is a trademark of the LEGO Group of companies which does not sponsor, authorize or endorse this site/presentation.