Photo by Ian Taylor / Unsplash

Reproducible Production Docker Builds with Devbox and Nixery

Jul 10, 2025

One of the issues I run into with docker is the constant lack of reproducibility. During development I've solved this by using devbox. Devbox doesn't solve this efficiently out of the box with their generated dockerfile. And using nix directly is not for the faint of heart.

I've been working on this problem and found a decent workaround using Devbox and Nixery that actually works.

The Problem

  • Docker builds aren't reproducible (based on ubuntu, alpine etc)
  • Devbox generated dockerfiles don't take advantage of buildLayeredImage in nix
  • It doesn't make sense for the team to learn nix (that's why we use devbox)

The Solution

Basically you can base your Dockerfile around Nixery using the devbox.lock commits for each package. This gives you the benefits of Nix's buildLayeredImage without having to write Nix code.

Nixery
The Virus Lounge

The devbox.json :

{
    "packages": [
        "elixir@latest",
        "nodejs@latest",
        "postgresql@latest",
        "ffmpeg@latest"
    ]
}

Then generate a Dockerfile that looks like this:

# Stage 1: Start with shell base
FROM nixery.dev/shell:076e8c6678d8c54204abcb4b1b14c366835a58bb AS base

# Stage 2: Elixir
FROM nixery.dev/elixir:7cc0bff31a3a705d3ac4fdceb030a17239412210 AS elixir-layer

# Stage 3: Node.js
FROM nixery.dev/nodejs:076e8c6678d8c54204abcb4b1b14c366835a58bb AS nodejs-layer

# Final stage: Use base and add packages
FROM base

# Copy nix store from each package stage
COPY --from=elixir-layer /nix /nix
COPY --from=nodejs-layer /nix /nix

# Create a profile directory and symlink all packages
RUN mkdir -p /nix/var/nix/profiles/default/bin && \
    for store_path in /nix/store/*; do \
        if [ -d "$store_path/bin" ]; then \
            for binary in "$store_path/bin"/*; do \
                [ -f "$binary" ] && ln -sf "$binary" "/nix/var/nix/profiles/default/bin/$(basename "$binary")" || true; \
            done; \
        fi; \
    done

ENV PATH=/nix/var/nix/profiles/default/bin:/bin:/usr/bin
WORKDIR /app

The magic is in those commit hashes. They come from your devbox.lock file, so every build uses the exact same package versions.

Why This Works

  • Those commit hashes mean identical packages every time. No more "it worked on my machine".
  • Docker can cache each package stage independently.
  • The packages are layered efficiently by Nixery
  • No Nix knowledge required: You just write normal devbox.json files. The script below handles the Nix complexity.

The Script

I made a shell script that does all this automatically. It reads your devbox.json and devbox.lock, extracts the commit hashes, and generates the Dockerfile for you.

A shell script that generates a dockerfile from a devbox.json and devbox.lock using nixery.dev
A shell script that generates a dockerfile from a devbox.json and devbox.lock using nixery.dev - devbox-generate-dockerfile.sh

The script is pretty straightforward and AI generated - it just parses the JSON files with jq and templates out the multi-stage Dockerfile. From there you modify it like any other Dockerfile.

Production Notes

You'd probably want to host your own Nixery instance instead of using the public nixery.dev. The concept is the same though.

This gives you the benefits of Nix's deterministic builds without having to learn Nix. Your Docker images are smaller, your builds are reproducible, and you don't have to deal with package drift anymore.

The current docker generate in Devbox works but isn't as optimal as this approach. This gets you closer to what buildLayeredImage does natively in Nix, just wrapped in a more familiar Docker workflow.

If you need help deploying, our team at Lambdo is ready to help.