Skip to content

Latest commit

Β 

History

History
207 lines (148 loc) Β· 9.09 KB

File metadata and controls

207 lines (148 loc) Β· 9.09 KB

πŸ“– Reading 11 (Bonus) β€” Reproducible Builds with Nix

🎁 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.


1. Why Reproducibility?

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 install pulls 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 sha256 digest?


2. Where Nix Came From

  • πŸŽ“ 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

3. The Big Idea: /nix/store

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)"]
Loading
  • πŸͺ¦ The traditional Unix /usr/lib/libfoo.so.2 has one active version. Nix's /nix/store has all versions β€” and that's the source of its power

4. The Simplest Nix Build

# 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.nix is a derivation: a deterministic recipe
  • πŸ”’ vendorHash forces 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 nixpkgs revision

5. Flakes: Pinning the World

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.lock and your repo is the reproducible build recipe

6. Reproducible Docker Images with Nix

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)

7. CI Caching with Cachix / Attic

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

8. The Honest Trade-offs

βœ… Nix wins ⚠️ Nix is hard
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 develop for project dev environments; add nix build for the project's binary; layer in dockerTools.buildImage last.


9. Real-World Use Cases

  • 🎯 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 dotcom infrastructure β€” uses Nix internally for reproducible builds of certain tooling

10. Lab 11 Preview

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 that nix build .#default produces a binary at result/bin/quicknotes that runs identically to the go build output
  • 🐳 Task 2 (4 pts): Use dockerTools.buildImage to 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

11. Resources

🎯 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.