nix-shell for reproducing bugs from five years ago
I don’t run NixOS. I don’t use home-manager. I don’t use flakes for my main projects. But there’s one very specific thing I use Nix for, and it’s earned its keep a dozen times over: reproducing bugs that depend on specific tool versions.
The use case
Customer reports a bug against v3.2.1 of our CLI, which shipped 2 years ago. We release weekly, we’re on v4.8.3 now, and building v3.2.1 from source requires the toolchain that was current two years ago — older Go, older protoc, specific versions of various CLI tools.
Getting that toolchain set up on my current Mac without breaking everything else I have installed is… not trivial. Homebrew has one version of each thing. asdf handles some of them but not all. Docker is overkill for a 15-minute bug repro.
nix-shell is the right tool for this. It’s a temporary, isolated environment with exactly the tools I specify, at exactly the versions I specify.
The minimum useful usage
# install nix (one time)
sh <(curl -L https://nixos.org/nix/install)
Then, in the repo you’re trying to reproduce:
# shell.nix
{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/abc123.tar.gz") {} }:
pkgs.mkShell {
buildInputs = [
pkgs.go_1_19
pkgs.protobuf
pkgs.protoc-gen-go
pkgs.sqlite
];
}
Then:
nix-shell
# you are now in a shell with exactly go 1.19, protoc, protoc-gen-go, sqlite
# your system tools are unaffected
which go inside the shell returns the Nix path, go version returns 1.19.x. Outside the shell, none of this exists. You can have multiple shells with different versions in different terminals. They don’t interfere.
The nixpkgs pinning is the magic
The fetchTarball "https://github.com/NixOS/nixpkgs/archive/abc123.tar.gz" line is the key. It says “use the nixpkgs from this exact commit.” Every package version is reproducible from that commit — two years from now, this shell.nix will still produce exactly go 1.19, because the nixpkgs at commit abc123 has exactly go 1.19, and it always will.
This is the problem Homebrew can’t solve for you, because Homebrew is a rolling release. “What version of ruby will brew install ruby give me today?” is a different answer from six months ago.
What I actually keep in my bag of tricks
I have a small collection of shell.nix files in ~/reproductions/ for various historical toolchains:
~/reproductions/
go-1.18-postgres-14/shell.nix
python-3.9-openssl-1.1/shell.nix
node-16-yarn-1/shell.nix
When a customer reports a bug against “our v2.1,” I grab the matching shell.nix (or write a new one in 5 minutes), nix-shell, check out the right commit, and I have a working environment.
For finding “which nixpkgs commit has this specific version,” https://lazamar.co.uk/nix-versions/ is invaluable. It’s a searchable index of package versions across nixpkgs commits.
The things I don’t use Nix for
- Dev environments for current projects. My team doesn’t use Nix. Asking everyone to learn it for a daily setup would cost more than it saves. Dev containers, asdf, and Homebrew cover this for us.
- Building production images. We use regular Dockerfiles. Nix-based Docker images are elegant but the build times and the team learning curve don’t pay off.
- System configuration. I’m happy with plain dotfiles.
Nix has strong opinions. For most of my work I don’t share those opinions, and that’s fine. But for “reproduce a past environment,” Nix is the best answer that exists.
The learning curve is real but bounded
I won’t pretend Nix is easy. The language is weird. Error messages are cryptic. The documentation is spread across a wiki, a manual, and a pile of blog posts. Learning the full Nix ecosystem probably takes weeks.
But for the use case I described, you need to learn ~20 lines of shell.nix syntax and nix-shell, fetchTarball, and mkShell. Maybe two hours. The rest you can pick up if you ever need to.
The flakes question
Nix flakes are a newer way to pin dependencies more rigorously. They’re great, they’re still marked experimental, they require a config flag. For my simple use case I don’t need them — the fetchTarball pattern is stable and sufficient. If you’re going deeper, flakes are probably the right target.
Reflection
Nix’s reputation for being hard keeps a lot of people away from it. Some of that reputation is earned. But the 20-line shell.nix pattern is one of those “small dose of a powerful drug” situations. You don’t have to buy the whole philosophy to benefit.
If you’ve ever had to reproduce a bug against an old toolchain, try this. Even once. The first time it saves you a day of “why won’t this build,” you’ll adopt it permanently for the narrow use case and ignore the rest.
Related: Debugging a remote core dump without losing your mind is another “keep this tool sharpened” piece.