Reproducible Development with Nix, Devbox & direnv

Modern teams juggle between M‑series laptops, Docker‑less CI runners and “tiny” side projects that each need a different Node version. Nix guarantees bit‑for‑bit reproducibility; Devbox hides Nix’s DSL behind a JSON manifest; direnv auto‑activates the env on cd. The trio erases “works‑on‑my‑machine” forever.


1 Why Nix?

Headache Nix fix
Global installs collide (brew upgrade node) Functional package manager → every package lives in its own hash path.
Onboarding new laptop takes a day Declarative lockfiles re‑create toolchains even years later.
CI passes, local fails Deterministic builds—same hash ⇒ same result.

2 Devbox: Nix power, one‑file UX

Devbox turns a short JSON file into a Nix derivation, builds it into /nix/store/‑env, prepends only those binaries to $PATH, and pins exact hashes in devbox.lock.

// devbox.json  – Node example
{
  "packages": [
    "nodejs@20",
    "bun",
    "sqlite"
  ],
  "scripts": {
    "start": "node index.js",
    "test": "bun test"
  }
}

Run devbox run start for non‑interactive or devbox shell for an interactive env.

CI in two lines

devbox generate github > .github/workflows/ci.yml
git add . && git push

The generated workflow restores only the hashes in devbox.lock, so cold starts are usually <1 min.


3 Installing everything with one curl (and no Homebrew)

curl -fsSL https://get.jetify.com/devbox | bash

The script installs Devbox and boot‑straps Nix if missing.

After installation, start a fresh shell so $PATH picks up /usr/local/devbox/bin.


4 direnv — Auto‑load the env on cd

Instead of asking every teammate to brew/apt direnv, we make Devbox do it globally:

devbox global add direnv          # installs direnv once for all projects
eval "$(devbox global shellenv)"  # puts global bins in PATH

Global packages live in ~/.devbox/global/devbox.json and sync across machines.

Create a project‑root .envrc:

# .envrc
use devbox        # provided by Devbox

Approve once:

direnv allow

Now simply cd into the repo—Node 20, bun, sqlite and any future additions materialise automatically, and vanish on cd ...


5 Daily workflow & tips

Scenario Command
Add a new tool (e.g., eslint) devbox add eslint + commit lockfile
Update all versions devbox upgrade → review diff → PR
Clean disk sudo nix-collect-garbage -d (local); CI runners auto‑prune.
New laptop run bootstrap script below, clone repo, done.

6 Bootstrap script (copy‑paste)

#!/usr/bin/env bash
set -euo pipefail

# 1. Install Devbox (brings Nix if absent)
curl -fsSL https://get.jetify.com/devbox | bash      # jetify installer

# 2. Start a new login shell so $PATH contains devbox
exec $SHELL -l

# 3. Install direnv globally via Devbox
devbox global add direnv                             # one‑time

# 4. Expose global packages to every shell
if ! grep -q 'devbox global shellenv' ~/.bashrc; then
  echo 'eval "$(devbox global shellenv)"' >> ~/.bashrc
fi
# add direnv hook
if ! grep -q 'direnv hook bash' ~/.bashrc; then
  echo 'eval "$(direnv hook bash)"' >> ~/.bashrc     # for zsh: replace bash→zsh
fi

echo "Reloading shell..."
exec $SHELL -l

Run it once per machine. The next time you git clone a Devbox‑enabled repo, direnv auto‑loads the exact environment, and CI uses the same hashes—no snowflakes left.


TL;DR

  • Nix gives deterministic builds.

  • Devbox makes Nix human‑friendly and CI‑ready.

  • direnv, installed via devbox global, removes the last manual step—envs load/unload automatically.

From fresh laptop to shipping code = one curl & one git clone.

Powered by blogkit. Inspired by Sairin.