Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Reproducible Environments

Why Docker Isn't Enough and Why Nix Might Be!


📦
Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

The "Works on My Machine" Contradiction

​🧑‍💻 It ran on production.

​🚚 Docker shipped the contents.

​🤔 So why does it not work now?

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

The Docker Myth

"I have a Dockerfile, so my environment is replicable."

Portable? Potentially. (Timing, Architecture, etc.) 🤔

Reproducible? Not really. ❌

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

The Reality of Drift

FROM node:24

RUN apt-get update && \
    apt-get install --assume-yes curl

​📅 apt-get update last month ≠ next month.

​🏷️ node:24 is a moving target.

​🌐 An outside network dependency leaks in.

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Docker: The Imperative Standard

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

How Docker Works

Each step depends on the results of the previous step.

Layer 1 ⬅️ Layer 2 ⬅️ Layer 3

Each instruction stacks a new layer.

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Docker: Strengths

​🌍 Ubiquity. A universal language of operations.

​🧱 Kernel-level isolation. Namespaces isolate processes; cgroups control resources.

​🚀 Ubiquitous registries. The registry is everywhere.

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Docker: Weaknesses

​🐘 Opaque bloat. Often a whole OS to run one program.

​🎲 Mutable base. When node:24 shifts upstream, your build shifts.

​🔁 Recipe, not a fingerprint. Dockerfile contains same steps often produce different outputs.

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Nix: The Functional Challenger

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

The Core Idea

A package is the results of a deterministic set of inputs.

Inputs: source, dependencies, compiler, flags.

Change any input ➡️ the content hash changes.

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

The Nix Store

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.

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Nix Flakes

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.

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Head to Head

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Docker vs. Nix

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
Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Synergistic Symbiosis

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Synergistic Symbiosis Strategy

​🏗️ Build the software with Nix.

​📦 Package it with Docker container images.

Nix computes the exact package. Ship it with Docker.

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Minimal Reproducible Docker Container Images for Free

pkgs.dockerTools.buildImage

Only the app and its exact dependencies.

The image will contain:

​🚫 No shell. 🚫 No package manager. 🚫 No stray CVEs.

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

My Two Cents

Ship a black box to production? ➡️ Docker.

Guarantee dev and CI are identical? ➡️ Nix.

Want both? ➡️ Build with Nix, ship with Docker.

Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Summary

  1. Portable ≠ deterministic ➡️ Docker ships the artifact.
  2. Output = f(Inputs) ➡️ Nix pins the build inputs.
  3. Flakes ➡️ Offer lockfile for the whole toolchain.
  4. Synergistic Symbiosis ➡️ Build with Nix, ship with Docker.
Reproducible Environments: Why Docker Isn't Enough and Why Nix Might Be! | JavaZone 2026 | Stø AS

Thanks!

sheeeng.github.io/slides

QR code for 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?