All dependencies, inputs, and ouputs live at:
/nix/store/<hash>-<name>
🔐 The hash is the fingerprint of every input.
🤝 Mutually exclusive program versions available for any package.
Offer lockfile for all external inputs:
# flake.nix
inputs.nixpkgs.url =
"github:NixOS/nixpkgs/nixos-26.05";
📌 flake.lock pins every input to an exact hash.
| Feature | Docker | Nix |
|---|---|---|
| Model | Imperative | Functional |
| Primary Goal | Build & Runtime Pseudo-Isolation | Deterministic Builds |
| Reproducibility | Opt-in Only: Rarely | Absolute, Bit for Bit, per OS/Architecture |
🏗️ Build the software with Nix.
📦 Package it with Docker container images.
Nix computes the exact package. Ship it with Docker.
pkgs.dockerTools.buildImage
Only the app and its exact dependencies.
The image will contain:
🚫 No shell. 🚫 No package manager. 🚫 No stray CVEs.
Ship a black box to production? ➡️ Docker.
Guarantee dev and CI are identical? ➡️ Nix.
Want both? ➡️ Build with Nix, ship with Docker.
sheeeng.github.io/slides
[30 seconds] Docker solved portability. It did not solve reproducibility. In this session, we look at why, and what does.
[45 seconds] We all know the line. Docker answered it by shipping the runtime environment, so the running artifact travels with you. That solved portability. It did not, on its own, make the build itself reproducible. Same recipe, different result, and that gap is the problem this talk addresses.
[45 seconds] Here is the comfortable myth. A Dockerfile feels like a reproducible specification. But a Dockerfile is a script of imperative steps, and it runs against a changing environment: package mirrors, the network, and "latest" tags. Portable means the image runs anywhere. Deterministic means the same inputs always produce the same output. Docker provides portability. Reproducibility requires more work, and we will see how.
[45 seconds] Look at three lines almost every image starts with. "apt-get update" pulls whatever the mirror serves right now, so today's versions differ from yesterday's. "node:24" is a tag, not a fingerprint, and it is republished upstream. And the build layer cache quietly hides all of this. Build this image in June and in December and you get two different machines from identical text. That is drift.
[15 seconds] So let us give Docker a fair hearing. Docker is the imperative model, and it earned that position for good reasons.
[45 seconds] Docker is imperative: do this, then this, then this. Each instruction adds a layer on top of the last, and the layers cache. That model is easy to read top to bottom and easy to teach. It is also exactly why it drifts, because every step depends on both the previous step's output and whatever the outside world provides.
[60 seconds] Docker's strengths are real. Everyone knows it, so it is a shared language between developers and operations. Internally, Linux namespaces give processes their own views of things such as the process tree, network interfaces, mounts, and hostnames. Cgroups limit and account for resources such as CPU and memory. Together, they create a practical runtime boundary without the overhead of a virtual machine. It is strong isolation, but not an absolute security boundary: containers still share the host kernel. And the registry ecosystem means an image can run on a laptop, in CI, and in production. This is why Docker won, and none of that is going away.
[45 seconds] The weaknesses follow from the same model. Without careful base image selection, even a simple app drags a full operating system along, which is bloat and, worse, attack surface. The base image is mutable, so upstream changes silently rewrite your foundation. And because the Dockerfile describes steps rather than a fingerprint of inputs, two builds of the same file can disagree. Save the image by digest and it is frozen. Rebuild from the same Dockerfile and the result may differ.
[15 seconds] Now the challenger. Nix comes at the same problem from the opposite direction: not "run these steps" but "compute this result."
[60 seconds] Here is the whole idea on one slide. Nix treats a build as a pure function. The inputs are everything that matters: the source code, every dependency, the exact compiler, the build flags, the environment. Nix hashes all of it into one identifier. That hash is computed before the build runs, which is how the binary cache works: if the hash already exists upstream, Nix downloads the result instead of building it. Same inputs, same hash, same output, every time. Change one input by a single byte and the hash changes, so you can always see which input changed. This is what the functional model gives you: builds you can reason about like math instead of like weather.
[45 seconds] Where do these outputs go? Into the Nix store, each under a path stamped with that input hash. Two consequences. First, the path itself proves what produced it. Second, because the hash disambiguates, many versions of the same library coexist without conflict. No more "which OpenSSL is active." Both versions coexist, each addressed by its own hash.
[45 seconds] Flakes make this practical. A flake declares your inputs, and the flake.lock pins each one to an exact Git commit hash. Check that lockfile into the repository and your teammate, your CI runner, and you in six months all resolve the identical dependency graph. It is the lockfile idea you know from application dependencies, raised to cover the whole toolchain.
[10 seconds] Let us put them next to each other.
[60 seconds] Read this as complementary, not as a scoreboard. Docker's model is imperative steps; Nix's is a declarative function. Docker's primary goal is isolating a running process; Nix's is making the build itself deterministic. Docker is reproducible only if you pin the image digest; rebuilding the same Dockerfile may produce a different result. Nix is reproducible from the inputs, bit for bit. The honest trade is the learning curve: Docker is approachable, Nix is steep.
[10 seconds] Here is the part I want you to remember.
[45 seconds] You do not have to choose sides. Use Nix to build the software, because it computes the exact closure of dependencies and nothing more. Then use Docker to package and distribute that result, because the registry and the runtime boundary are excellent. Determinism on the way in, ubiquity on the way out. For day-to-day use, `nix develop` gives every developer an identical shell with the same JDK, build tool, and linter. No installation instructions are needed.
[45 seconds] The payoff is concrete. Nix can emit a Docker image that contains only your application and the exact store paths it needs. No shell, no package manager, no half of an operating system included. That is a small, minimal image with far less attack surface, produced deterministically.
[45 seconds] So the decision rule. If your job is to hand a running black box to production, a Dockerfile is a fine and familiar answer. If your job is to guarantee that your development machine and your CI pipeline are identical at every level, that is where Nix excels. And on a serious project, the answer is usually both, in that order.
[30 seconds] Four things to remember. Portable is not the same as deterministic; Docker gives you the first. Nix models the build as a function of its inputs, so it gives you the second. Flakes make that a lockfile for your whole toolchain. And the two compose well: build with Nix, ship with Docker.
[15 seconds] Thank you! Slides are on GitHub at that link. Leave with the one line that sums it up: Docker packages the mess, Nix fixes the mess. Questions?