“It works on my machine” has a container-shaped solution; “it produces the same Sharpe” does not. Docker pins one axis of the reproducibility triple — the environment — completely and portably. This guide is the research-flavored pattern, and an honest accounting of what containers don’t fix.

What the container actually pins

A research image freezes: OS and system libraries (the BLAS/LAPACK builds that make numerical results differ across machines), the language runtime, and — via a lockfile inside the build — exact package versions. The contract: any machine, any year, docker run → the same bits execute. That kills the classic failures: the dependency upgrade that silently changed results, the colleague who can’t build your stack, and the six-months-later rerun on a machine that no longer exists.

The research-image pattern, minimally:

FROM python:3.12-slim@sha256:<digest>        # digest, not tag — tags move
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen        # lockfile is the law
COPY src/ ./src                               # code last: layer caching
# no data in the image — data mounts, versioned separately

Three rules doing the work: base images by digest (:3.12-slim is a moving target); lockfile-frozen installs (the image build must not resolve dependencies — resolution is the nondeterminism you’re killing); and data stays outside (mounted, with its own versioning) — images with baked-in data conflate two version axes and bloat unmanageably.

What Docker does not solve

The list that separates container users from reproducibility havers:

  • Seeds and algorithmic nondeterminism: same image, unseeded RNG → different results; GPU kernels and multi-threaded reductions add nondeterminism a container happily preserves. Seed everything; pin thread counts (OMP_NUM_THREADS and friends in the image’s env); know which ops are nondeterministic by design.
  • Data versions: the container reproduces the computation, on whatever data you mount — manifest-hashed snapshots are a separate discipline.
  • Config drift: parameters live outside the image (as they should); the run row records them.
  • Hardware-order effects: float summation order differs across core counts; bit-identical across machines sometimes requires pinning parallelism too. Decide your tolerance (stated, like a reconciliation) before declaring divergence a bug.

The honest overkill line

Containers earn their keep at: handoff boundaries (a replication package someone else must run), production (where they’re already the runtime — research/live parity is a real win), many-machine work (cloud GPU rentals where the image is the setup), and long-horizon archival of published results. For a solo researcher iterating locally, a lockfile + pinned interpreter delivers most of the value at none of the friction — the honest default, with the Dockerfile added the day any boundary above appears. What’s not optional at any scale is the lockfile itself; “pip install pandas” in a README is an unversioned experiment.

Common containerized-research failures

  • The tagged base image: FROM python:3.12 rebuilt a year later resolves different digests — reproducible-looking, wasn’t.
  • Resolution inside the build: no lockfile → every build a new environment wearing the same name.
  • The data-in-image antipattern: 40GB images, version soup, and backup policies that can’t tell code from data.
  • Container in research, bare metal in production: the parity win inverted — two environments, one strategy, divergence by construction.
  • Unpinned threads on shared runners: CI produces different numerics than the workstation; the “bug hunt” is a core-count diff.

Questions to ask

  1. Does a from-scratch build today produce the same environment as six months ago? (Digest + lockfile = yes.)
  2. Same image, same data, same seed, twice: identical outputs? If not, what’s the stated tolerance?
  3. Is the image digest in the run row?

What our scoring captures

Environment specification is a rigor micro-signal with macro correlation: papers shipping Dockerfiles or lockfiles nearly always clear the bigger reproducibility bars too — it’s the visible tip of the discipline stack. In our archive the pattern concentrates in the ML hub’s top tier, where reruns are cheapest to demand and rarest to survive.


The other axes: versioning data & runs → · Where images run: production stack → · cloud GPUs →