Back to Linux guides

Devbox: Reproducible Development Environments with devbox.json and Nix

Stanley Ulili
Updated on June 1, 2026

Devbox is a command-line tool that creates isolated development shells from a devbox.json file. It uses the Nix Package Manager under the hood but exposes a simple interface without requiring knowledge of the Nix language. Package versions are resolved to a devbox.lock file, so any developer running devbox shell from the same repository gets bit-for-bit identical tool versions.

How it works

Terminal window displaying a cascade of setup errors for Node.js, Python, PostgreSQL, and Docker illustrating the failure of a typical README setup guide

Devbox installs packages in a sandboxed environment, not globally. When you run devbox shell, the specified tools are placed on your PATH for the duration of the session. When you exit, your global environment is unchanged.

Diagram showing the relationship between devbox.json (the declarative file) and devbox.lock (the resolved file) which together create identical builds everywhere

Two files control the environment:

  • devbox.json: the human-readable declaration you edit (for example, "nodejs@20", "go@1.22")
  • devbox.lock: auto-generated by Devbox with exact Nix package hashes

Committing both to the repository ensures that any developer or CI job using the same commit gets the same environment.

The Nix package repository contains over 400,000 package versions, including specific minor versions of most programming languages, databases, and CLI tools. Multiple versions of the same tool can coexist on a single machine without conflict because Nix isolates them at the filesystem level.

Installation

 
curl -fsSL https://get.jetpack.io/devbox | bash
 
devbox version

Setting up a project

Initialize a devbox.json in any project directory:

 
devbox init

The generated file starts with an empty packages list:

devbox.json
{
  "packages": [],
  "shell": {
    "init_hook": null,
    "scripts": {}
  }
}

Add packages with version pins:

 
devbox add go@1.22
 
devbox add nodejs@20
 
devbox add python@3.11

After adding these, devbox.json looks like:

devbox.json
{
  "packages": [
    "go@1.22",
    "nodejs@20",
    "python@3.11"
  ],
  "shell": {
    "init_hook": null,
    "scripts": {}
  }
}

These packages are not installed globally. They are registered as dependencies for this project only.

Enter the isolated shell:

 
devbox shell

The first run downloads packages from Nix and caches them. Subsequent starts are instantaneous. Once inside the shell, go version, node --version, and python --version reflect the pinned versions. Running exit returns to the normal environment.

To search for available packages:

 
devbox search postgresql

Scripts

The scripts section in devbox.json defines common project commands, similar to scripts in package.json:

devbox.json
{
  "packages": ["go@1.22", "nodejs@20", "python@3.11"],
  "shell": {
    "init_hook": null,
    "scripts": {
      "test": "echo 'Running tests...' && node --version && go version"
    }
  }
}

Running a script:

 
devbox run test

This starts a Devbox shell, executes the script in the correct environment, and exits. Scripts always run with the project's pinned tool versions regardless of what is installed globally.

init_hook for setup automation

init_hook runs commands each time a developer enters the shell. It is useful for installing language-specific dependencies or setting environment variables.

For simple cases, inline commands work:

devbox.json
"init_hook": ["npm install"]

For complex setup logic, calling a shell script keeps the JSON readable:

Diagram showing complex logic moved from a JSON string into a clean readable .sh file which is then called from the init_hook

setup.sh
#!/bin/bash
set -e
echo "Installing dependencies..."
npm install
echo "Environment is ready!"
devbox.json
"init_hook": ["./setup.sh"]

The setup.sh approach makes the setup logic version-controllable and reviewable as a normal file rather than an embedded string.

Generating a Dockerfile

If the project requires a Docker image (for example, for VS Code Dev Containers), Devbox can generate one from devbox.json:

 
devbox generate dockerfile

This produces a Dockerfile that installs the same packages specified in the project's devbox.json, giving a single source of truth for both the local shell and the container image.

CI/CD integration

Because devbox.lock pins exact versions, running devbox shell in a CI job produces the same environment as a developer's local machine. This eliminates the class of failures where a test passes locally but fails in CI due to a different tool version.

Final thoughts

Devbox solves the version management problem without requiring Docker for local development. It is faster than containers for day-to-day shell use, integrates with Git in a straightforward way, and does not require learning Nix directly.

The two-file model (devbox.json for declarations, devbox.lock for resolution) is the same pattern used by most modern dependency managers and is easy to reason about in code review. When a dependency is updated, the diff in devbox.lock shows exactly what changed.

For teams where setup time and "works on my machine" issues are recurring costs, the one-time effort of adding a devbox.json to each project pays back quickly.

Documentation is at jetpack.io/devbox.

Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.