π This is a bonus reading, paired with Lab 11. The 10 main lectures are required; this reading is for students who want to go deeper on reproducible-builds territory.
Every container, every artifact, every binary you've shipped in this course had an invisible assumption: "if I rebuild it, I'll get the same thing back."
That assumption is wrong for ~90% of real-world software. Build the same Dockerfile twice, even from the same Git SHA, and you typically get:
- π Different timestamps baked into binaries
- π¦ Different transitively-resolved package versions (
apt-get installpulls today's Ubuntu mirror, not last week's) - π§ͺ Different test output that depended on
now()or$HOSTNAME - πͺͺ Different layer SHAs in your image β even if the contents are equivalent
π¬ "A reproducible build is a build whose output is bit-for-bit identical when run from the same source by anyone, on any compatible machine." β reproducible-builds.org
Why care?
- π‘οΈ Supply chain trust: if anyone can reproduce your artifact from source, no attacker can secretly slip a backdoor in (xz-utils 2024 lesson). Cosign verifies a signature; reproducibility verifies the content
- π Bisecting at scale: "this commit broke prod" requires you to be able to rebuild that commit cleanly. Heisenbugs in build environments are the hardest to debug
- π¦ Deterministic deploys:
image:sha256:abc...deployed today is exactly the same bits deployed last year β invaluable for incident response and rollback
π€ Think: Can you, today, rebuild the QuickNotes container image from a Git tag and get the exact same
sha256digest?
- π 2003 β Eelco Dolstra publishes "Nix: A Safe and Policy-Free System for Software Deployment" (PhD thesis, Utrecht University)
- π¦ 2003-2010 β NixOS (the Linux distribution built on Nix) takes shape
- π 2015-2020 β Nix gets traction outside academia: dev environments, CI caching, reproducible builds
- π 2020-2021 β Nix Flakes introduced (experimental β standard) β locked dependencies, pure evaluation, much better UX
- π§ͺ 2023-2026 β Nix is used in production at Tweag, Anduril, NixOS Foundation members; determinate-nix ships an opinionated, supported installer
Every package, every binary, every config file, every dependency lives at a path like:
/nix/store/abc123...-quicknotes-0.1.0/bin/quicknotes
- π The prefix
abc123...is a hash of all inputs that produced this output β source, dependencies, build environment, even the compiler version - π§± Change anything in the recipe β different hash β different path β both versions coexist
- π No conflicts: ten versions of QuickNotes can live side-by-side, each in its own directory
graph LR
S["π derivation (recipe)"] --> H["π’ hash inputs"]
H --> P["/nix/store/HASH-name"]
P --> B["π¦ build outputs<br/>(immutable)"]
- πͺ¦ The traditional Unix
/usr/lib/libfoo.so.2has one active version. Nix's/nix/storehas all versions β and that's the source of its power
# default.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.buildGoModule {
pname = "quicknotes";
version = "0.1.0";
src = ./.;
vendorHash = "sha256-AAAA..."; # pinned hash of vendor/ tree
CGO_ENABLED = 0;
}$ nix-build # builds quicknotes, output symlinked at ./result
$ ./result/bin/quicknotes- π The
default.nixis a derivation: a deterministic recipe - π’
vendorHashforces Nix to verify the entire Go vendor tree byte-for-byte - πͺ Run on your laptop, your colleague's laptop, CI β they all produce the same hash for the same source + same
nixpkgsrevision
Flakes (Nix 2.4+, standard since ~2024) lock all external dependencies β including the nixpkgs snapshot β to specific commits.
# flake.nix
{
description = "QuickNotes β DevOps-Intro project";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
outputs = { self, nixpkgs }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
packages.x86_64-linux.default = pkgs.buildGoModule {
pname = "quicknotes";
version = "0.1.0";
src = ./.;
vendorHash = "sha256-AAAA...";
CGO_ENABLED = 0;
};
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [ pkgs.go pkgs.gopls pkgs.golangci-lint ];
};
};
}$ nix flake init
$ nix build .#default
$ nix develop # enters a shell with Go 1.24, gopls, golangci-lint pinned- π
flake.lock(auto-generated) pins every input to a SHA β same build forever - π¦ Commit
flake.nix+flake.lockand your repo is the reproducible build recipe
Nix's dockerTools.buildImage builds a container image without Docker, deterministically:
pkgs.dockerTools.buildImage {
name = "quicknotes";
tag = "v0.1.0";
config = {
Entrypoint = [ "${self.packages.x86_64-linux.default}/bin/quicknotes" ];
ExposedPorts = { "8080/tcp" = {}; };
};
}- π₯ͺ No
FROM. The image is exactly the runtime closure of QuickNotes β usually ~10-30 MB - π’ Same source + same nixpkgs revision β bit-for-bit identical OCI image with the same digest
- π³ Push to any OCI registry:
docker load < $(nix build .#docker --print-out-paths)
Nix builds are slow on cold caches. The fix: a binary cache.
| Service | Hosted? | Cost | Notes |
|---|---|---|---|
| Cachix | β | Free tier; paid for private | Push from CI: cachix push myproj /nix/store/... |
| Attic | Self-host | Free | Self-hosted binary cache |
| GitHub Actions cache | Built-in | Free | Limited size; works but not ideal |
- π With a warm cache, a Nix build of QuickNotes is faster than
docker buildβ because every dependency is a hash lookup, not a rebuild - π Reproducibility means the cache is shared safely β if hashes match, the bits match
| β Nix wins | |
|---|---|
| Bit-for-bit reproducible builds | Learning curve β the language is unusual |
| Per-project dev shells (no global pollution) | Build errors are dense, scary |
Tiny container images without FROM |
Slow first build (no cache) |
| Atomic upgrades & rollbacks (NixOS) | Some ecosystems (Node, Python) have rough edges |
| Same recipe builds on Linux + macOS | macOS support has been historically rougher |
π‘ A pragmatic adoption path: start with
nix developfor project dev environments; addnix buildfor the project's binary; layer indockerTools.buildImagelast.
- π― Tweag β consulting firm, uses Nix end-to-end for client projects since ~2015
- πͺ Anduril β Nix in CI for defense-grade reproducibility
- π NixOS β the Linux distribution that proves the model end-to-end (the entire OS is one Nix expression)
- π¦ Many infra teams β adopt Nix for dev environments first, then expand
- π€ GitHub
dotcominfrastructure β uses Nix internally for reproducible builds of certain tooling
Lab 11 is the bonus lab. Two tasks (no Bonus row β the whole lab is bonus):
- ποΈ Task 1 (6 pts): Convert the QuickNotes Go build to a Nix
flake.nix. Build it. Show thatnix build .#defaultproduces a binary atresult/bin/quicknotesthat runs identically to thego buildoutput - π³ Task 2 (4 pts): Use
dockerTools.buildImageto build a Nix-native OCI image of QuickNotes. Demonstrate two builds (from different CWDs / clones) produce the identical sha256 digest - π Deliverable:
submissions/lab11.mdβ flake.nix, build output, two digests proving reproducibility, written analysis of the experience
- π Nix Pills β nixos.org/guides/nix-pills (free, the canonical intro)
- π Zero to Nix β zero-to-nix.com (Determinate Systems' modern walkthrough)
- π NixOS & Flakes Book β nixos-and-flakes.thiscute.world
- π₯ Domen KoΕΎar β Boost your dev env with Nix
- π Reproducible Builds project β broader than Nix, but Nix is the most complete answer
- π Eelco Dolstra's original Nix paper (2004) β for the academic curious
π― Remember: Reproducibility isn't a Nix feature. It's a goal. Nix is one (excellent) implementation of it. The 2024 xz-utils backdoor lived in source for two years β bit-for-bit-reproducible builds + community-signed attestations would have caught the divergence the day it landed.