Reproducible Environments | JavaZone 2026

Reproducible Environments

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


📦
Reproducible Environments | JavaZone 2026

The "Works on My Machine" Paradox

​🧑‍💻 It runs on my laptop.

​🚚 Docker shipped the machine.

​🤔 So why did the build break in CI?

Reproducible Environments | JavaZone 2026

The Docker Myth

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

Portable? Yes. ✅

Deterministic? Not by default. ❌

Reproducible Environments | JavaZone 2026

The Reality of Drift

FROM node:18
RUN apt-get update && apt-get install -y curl

​📅 apt-get update today ≠ yesterday.

​🏷️ node:18 is a moving target.

​🌐 Caches and network state leak in.

Reproducible Environments | JavaZone 2026

Docker: The Imperative Standard

Reproducible Environments | JavaZone 2026

How Docker Works

A recipe of ordered steps.

Layer 1 ➡️ Layer 2 ➡️ Layer 3

Each instruction stacks a new layer.

Reproducible Environments | JavaZone 2026

Docker: Strengths

​🌍 Ubiquity. The universal language of operations.

​🧱 Strong isolation. Cgroups and namespaces give a hard boundary.

​🚀 Ships anywhere. The registry is everywhere.

Reproducible Environments | JavaZone 2026

Docker: Weaknesses

​🐘 Opaque bloat. A whole OS to run one script.

​🎲 Mutable base. If node:18 shifts upstream, your build shifts.

​🔁 Recipe, not a fingerprint. Same steps, different output.

Reproducible Environments | JavaZone 2026

Nix: The Functional Challenger

Reproducible Environments | JavaZone 2026

The Core Idea

A package is the output of a pure function.

Inputs: source, dependencies, compiler, flags.

Change any input ➡️ the content hash changes.

Reproducible Environments | JavaZone 2026

The Nix Store

Everything lives at:

/nix/store/<hash>-<name>

​🔐 The hash is the fingerprint of every input.

​🤝 OpenSSL 1.1 and 3.0 coexist. No conflict.

Reproducible Environments | JavaZone 2026

Nix Flakes

A lockfile for your entire environment.

inputs.nixpkgs.url =
  "github:NixOS/nixpkgs/nixos-25.05";

​📌 flake.lock pins every input to an exact commit.

Reproducible Environments | JavaZone 2026

Head to Head

Reproducible Environments | JavaZone 2026

Docker vs. Nix

Feature Docker Nix
Model Imperative Functional
Primary goal Runtime isolation Build determinism
Reproducibility High, if saved Absolute, bit for bit
Learning curve Low to moderate Steep
Environment Virtualized Native binaries
Reproducible Environments | JavaZone 2026

Better Together

Reproducible Environments | JavaZone 2026

The "Better Together" Strategy

​🏗️ Build the software with Nix.

​📦 Package it with Docker.

Nix computes the exact closure. Docker ships it.

Reproducible Environments | JavaZone 2026

Distroless, for Free

pkgs.dockerTools.buildImage

Only the app and its exact dependencies.

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

Tooling: dockerTools, devenv.sh, Flox.

Reproducible Environments | JavaZone 2026

When to Reach for Each

Ship a black box to production? ➡️ Docker.

Guarantee dev and CI are identical? ➡️ Nix.

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

Reproducible Environments | JavaZone 2026

Summary

  1. Portable ≠ deterministic ➡️ Docker ships the artifact.
  2. Output = f(Inputs) ➡️ Nix pins the build.
  3. Flakes ➡️ A lockfile for the whole toolchain.
  4. Better together ➡️ Build with Nix, ship with Docker.
Reproducible Environments | JavaZone 2026

Thanks!

"Docker packages the mess; Nix eliminates the mess."

sheeeng.github.io/slides

❄

[30 seconds] Welcome! Quick show of hands: who has ever said "it works on my machine"? Keep your hand up if a Dockerfile did not fully save you. Over the next 20 minutes we compare the imperative and functional approaches to environments, and see where each one earns its keep.

[45 seconds] We all know the line. Docker answered it by shipping the whole machine, 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 the gap between "runs" and "rebuilds the same" is where we live today.

[45 seconds] Here is the comfortable myth. A Dockerfile feels like a reproducible spec. But a Dockerfile is a script of imperative steps, and it runs against a moving world: package mirrors, the network, and "latest" tags. Portable means the image runs anywhere. Deterministic means the same inputs always produce the same output. Docker gives you the first for free. The second you have to earn.

[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:18" 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. This is the imperative standard, and it is the standard 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 trusts the state left by the step before it and the world outside it.

[45 seconds] Docker's strengths are real. Everyone knows it, so it is the common tongue between developers and operations. Cgroups and namespaces give a genuine runtime boundary around your process. And the registry ecosystem means an image runs the same on a laptop, in CI, and in production. This is why Docker won, and none of that is going away.

[45 seconds] The weaknesses are the flip side. A tiny Python script often 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. Docker pins the artifact well. It does not pin the build.

[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. Same inputs, same hash, same output, every time. Change one input by a single byte and the hash changes, so you can always see what moved. This is what "functional" buys 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 live side by side without fighting over one global slot. No more "which OpenSSL is active." Both are, addressed by their 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 your future self 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 if you save the image; Nix is reproducible from the inputs, bit for bit. The honest trade is the learning curve: Docker is approachable, Nix is steep. And Nix runs native binaries from the store rather than shipping a virtual machine's worth of userland.

[10 seconds] Here is the part I actually 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.

[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 riding along. That is a small, honest image with far less attack surface, produced deterministically. And if the raw Nix language feels steep, devenv.sh and Flox give you the reproducibility with a gentler on-ramp.

[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 down to the last byte, that is Nix's home turf. And on a serious project, the answer is usually both, in that order.

[30 seconds] Four things to carry out the door. 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 beautifully: 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 eliminates the mess. Questions?