⚠️ NOTE: This guide is macOS and Linux only. The software mentioned is NOT natively supported on Windows and therefore can only be used in WSL.
This seeks to be an approachable guide for developers who:
- have experienced loading up an old project and it no longer builds/runs or
- are curious about how to upgrade their setup from using one-off version managers like nvm or
- have heard about Nix but haven’t tried it yet or
- have tried to use Nix but found it to be unapproachable
Devbox creates isolated, reproducible development environments that run anywhere. No Docker containers or Nix lang required.
“Isolated” means that you can have two projects, e.g. ~/src/projectA and ~/src/projectB, that have different development environments with different software packages without interfering with each other.
“Reproducible” means that when you enter a Devbox shell, you will always get the same exact software packages as when you were last working on that project. And if you have collaborators, you’ll all be using the same development environment.
Have you ever had the experience of stepping back into an old project and nothing works anymore? You can’t build, deploy, or do anything with it, even though everything worked the last time you were working on it. Development environments that use your system’s software will eventually rot because of software drift. For example, you might install a newer version of
node that is no longer backwards-compatible with the code in your project, or you might not have installed
sharp when you setup your new laptop and your project depends on it.
A Devbox project will work a decade from now just as well as it does today.
If you’re working on five separate projects there’s no reason they all need to use the same global version of every piece of software. Maybe one project uses Ruby 2 and another project uses Ruby 3. Should you have to upgrade all your projects in one go? Do you communicate to your collaborators that they need to update their systems and all their projects in one go? Is that even feasible?
Devbox allows your projects to co-exist while using different versions of software.
There are many solutions to having isolated, reproducible development environments. Two more popular solutions are remote developer boxes (usually powered by containers or VMs) and Docker Desktop.
There are many downsides to having your development environment in a VM (which is what Docker Desktop uses on macOS and Windows), such as deciding how much memory to allocate to the VM, the performance hit of using a VM, the disconnect of having to mount your project files in the VM, the battery life (or even just power efficiency) hit of running a VM, etc.
Remote development has so many headaches I’m not going to enumerate them.
Devbox makes it so that you are running local, native software, you’re using your regular file system, etc. There are no VMs or containers to get in the way and your mental model for where and how software is running can be extremely simple.
Does your team maintain a wiki where you keep instructions on how to setup a development environment? Is it always out of date?
Does your open source project give instructions on how to setup the software required to build and run your project?
With Devbox, if you test a new version of a package and it works, all you have to do is commit the changes to
devbox.lock to your project. If you push those changes to a shared repository, your collaborators that pull those files will start using the new version of the package you tested, without any discussion required. Everyone will stay in sync, automatically.
You interact with Devbox through the CLI, searching for and installing packages. For example, to install the test
hello program, you might run:
This is NOT installing anything globally. It is saying “if I start a Devbox shell in this directory,
hello should be available” (and defaults to the latest version). You can see that by trying to use the test
hello program outside of the Devbox shell:
Highly optional section: feel free to skip!
Adding packages to your Devbox project modifies two files:
devbox.json- a human readable/editable manifest of what packages and configuration you have for the Devbox shell in that directory:
devbox.lock- this is analogous to something like a
package-lock.jsonfile, where the exact version of the software you requested is stored away to be re-used later. This makes it so that even if you install a package with something like
devbox add nodejs@latest, the exact version will be stored in
devbox.lockand re-used later anytime you enter the Devbox shell, so you will still always get the same version.
devbox.jsonmaps to the same
devbox.lock, in this case
hello@latest. In the
devbox.lockfile it stores what precise package that resolved to, in this case:
github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#hello". That maps to a precise version of
hellothat you can find right on GitHub.
When you run
devbox shell, Devbox will check your
devbox.json, observe that you want
hello@latest, find that in
devbox.lock, lookup the exact version, download and build the package as necessary, and make it available in your shell. You can see the version of
hello will match the version in your
Both of these files should be committed to version control.
And if you want to find out where the package lives:
Which is a hard-link into the Nix store:
Nix is a programming language (nixlang), an operating system (NixOS), and a package manager (nixpkg)2.
The package manager is the largest in the world and, under the covers, powers all the software you are using with Devbox. Devbox exists to hide the complexity of Nix while exposing a small but very powerful set of ideas that are super useful.
Nix is there under the covers, but you don’t need to know anything about it at all. Devbox is not a leaky abstraction: you can use Devbox and never think about or understand a single aspect of Nix, ever.
Install on your machine with:
Initialize Devbox in an existing project:
Test that everything works:
If you’re in a repository, commit the Devbox artifacts to version control:
There are two ways to search for packages with Devbox. One is directly from the CLI:
Having to manually type
devbox shell isn’t a huge burden but making Devbox totally automatic with direnv is a convenience I highly recommend.
Imagine this: you run
git pull and get updates to
devbox.json. Without a single keystroke, you’re now reliably using the same version of every software package as the person that merged the
devbox.json updates, because Devbox automatically noticed Node.js had a new version in
devbox.json and upgraded it for you.
Using direnv is a simple two step process:
- Install direnv: https://direnv.net/#basic-installation
- In any Devbox project you want automatic shells, run
devbox generate direnv.
Now when you
cd into your directory, your devbox shell will automatically activate!
Devbox publishes a great VSCode extension that, among other things, makes the integrated terminal automatically use your Devbox shell. You can add this extension to your recommended workspace extensions to make things more seamless for your collaborators.
The extension is potentially redundant to using direnv, depending on your setup. I personally install both.
You can read more about it in the official docs.
I lightly recommend always specifying the package version when adding a package, so that
devbox.json is self-documenting and you never have to cross-reference it with
devbox.lock (which I personally never open). What that looks like:
If you don’t specify the version, it will be set to
latest. The version will be pinned either way4, but a package marked as
latest will be upgradeable by running
devbox update, and a package pinned to a specific version has to be manually updated. I always manually update my packages, I like seeing the versions right in
devbox.json, and other people might be confused about what
latest means like I was, so I always manually specify the versions when adding packages.
Devbox can be used as a global package manager, completely replacing traditional package managers like Homebrew for globally installed packages. That’s not required: you can use Devbox for your projects and Homebrew for global packages, if you’d like. They work nicely side-by-side.
If you use Devbox for global packages, you have two levels of “global” you can decide between:
- Option 1: You want a set of packages to always be available in any Devbox shell, without having to explicitly add them. For example, if you don’t want to have to
devbox add gitin every project, you can
devbox global add gitand it will be available in any Devbox shell (and not available in a vanilla shell).
- Option 2: Same as above, but you don’t even want to have to explicitly enter a Devbox shell. In that case, use
devbox global shellenv(e.g. put
eval "$(devbox global shellenv)"in
~/.zshrc). That will make
git(or any other
devbox global addpackages) available in any shell you start.
My decision making process is:
- If I need a package in a project, I install through Devbox. 98% of the packages I install are in a project (e.g.
devbox add nodejs).
- When I occasionally need a package outside of any projects, I install it with Devbox global (e.g.
devbox global add nodejs). This is rare and to be used judiciously: my global list is currently only four packages:
nodePackages.pnpm. Note that I still add these packages in projects that need them. I only use the global versions as fallbacks when I’m outside a project.
- When I need a package available universally, anywhere in my operating system, whether I started in a shell or not, I use Homebrew. For example, if some GUI application needs
git(like Source Tree) and I can’t be bothered to start that application from a shell, I just install
gitthrough Homebrew so I can not think about it. This is extremely rare.
For the visually inclined:
Read more about Devbox global packages on the official docs.
Devbox relies on two files:
devbox.lock. You should decide what you want to do with these files:
- If you’re a solo developer, commit them.
- If you collaborate with others and most of them want to use Devbox, commit them.
- If you collaborate with many other people and they don’t have any objections to committing configuration they don’t need, commit them. If someone else doesn’t have Devbox they can just ignore these files, and deal with the pain of manually installing software themselves. A Devbox project is progressive enhancement in action: it is invisible to those that want to ignore it, and very helpful to those that are prepared to use it.
- If you collaborate with others and they don’t want this “junk” in their repo (or you don’t even want to ask for permission), don’t commit them. You can either just leave them there (for one-off PRs to open source projects, for example), or you can add them to
.gitignore(I haven’t met anyone anally retentive enough to object to cluttering the
The benefits of a team using Devbox should be obvious: if one person upgrades Node.js, fixes any related bugs, and commits the new tested version into
devbox.json, no one else will have to repeat that work. Instead of updating your “dev machine setup guide” wiki page or posting “hey upgrade Node” in your team’s Slack channel or whatever, everyone will just get the new version of Node.js automatically when they
git pull the new
|Devbox||Devenv||Nix||Docker||Homebrew / Chocolatey|
|Use packages by semver5||✅||❌||❌||✅||✅|
|Automatic Shell Activation (direnv)||✅||✅||✅||✅||n/a|
|Does not depend on VM||✅||✅||✅||❌||✅|
|Share config in version control||✅||✅||✅||✅||❌|
|Ease of use||✅||❌||❌||❌||✅|
|Create Containers (share with production)||✅||✅||✅||✅||❌|
|Windows support||❌ 7||❌||❌||✅||✅|
|Global packages||⚠️ 8||❌ (?)||✅||❌||✅|
- ✅ With Devbox, per-project package isolation is easy. With Homebrew, everything is global.
- ❌ Sometimes you just want a package, and you want it everywhere. For example: are you using a tool you didn’t start from a Devbox shell, and it expects git to be available. It won’t find it. Devbox doesn’t excel at this.
- ✅ Devbox, by virtue of using Nix, has more packages then any other package manager.
- ✅ Devbox developer environments are more reproducible. Even the modified date of the package binary will be the same on your system and your collaborators!
- ❌ I’ve seen new package versions be available in Homebrew more quickly than Devbox (e.g. bun.sh). That’s not common and major packages like Node.js are available very quickly with Devbox, so this has never been an issue for me, but it’s something you can check on if it’s important to you.
I’ve never liked the circuitousness of using Docker Desktop for my dev environment. It solves a lot of problems well but it seems like Devbox solves those problems much better, without having a layer of indirection through a virtual machine. I want to just
cd into my project directory without having to bind mount my code into the container. I don’t want to have to even think about that layer of indirection.
Which is all to say I’m not even going to really compare the two.
devenv solves the same problem as Devbox: creating per-project, isolated development environments. The biggest difference is that Devbox totally abstracts Nix away from you while devenv is a leakier (and therefore more powerful) abstraction that lets you use more of Nix by requiring that you use Nix (and know how to use Nix). Instead of a very simple array of package names as strings, you write a
devenv.nix file using the Nix language.
The Devbox founder said it well:
“Devbox is trying to give you the power of nix, but with a simplified interface that matches the simplicity of a package manager like yarn. If you want reproducible environments by simply specifying the list of packages you depend on, and not having to learn a new language, then Devbox is a great match.”
Note that you can install packages from Nix flakes with Devbox, which gets you a ton of that power, but it’s more of a side feature than part of the core of how it works. I’ve used a custom flake with Devbox exactly once, when I need to install the Mercurial package with the
hg-evolve extension (which isn’t available by default).
Oh, and Devbox just gives you package semver so you don’t have to think about Nix package names and how that maps to versions. If I want
email@example.com I don’t want to think about which Nix package has that version of Node.js, which is another benefit over devenv.
If you’re capable of using Nix to manage all the packages on your system, all shall love you and despair.
This is probably not a comprehensive list of problems you will hit when using Devbox, but it includes all of the minor issues i’ve encountered:
- When I always need something available, anywhere on my system, whether in a Devbox shell or not, I use Homebrew. Example:
- The absolute paths to packages with Devbox will vary by project, which can be annoying when you need a stable path to a binary that is always available. The relative path is stable, and the
DEVBOX_PACKAGE_DIRenvironment variable is set to point to all the binaries, so this is unlikely to be an issue.
- Devbox will hurt your shell startup time. It’s something they’ve made a lot better since the first release, and it’s good enough for me, but the cost is not zero.
- Using direnv with Devbox is essential to the great experience, but direnv dumps a lot of noise to the console. This is a direnv issue and can’t be fixed by Devbox. Here’s what entering the console for this website’s project looks like for me: (That’s a lot more annoying with word wrap turned on, i.e. in most terminals)
- Devbox hasn’t hit 1.0 yet and is therefore not 100% mature and stable. I’ve reported my share of issues. That said, when I report an issue the dev team is super responsive and kind and it’s been a very long time since I’ve had an issue. I consider it an indispensable and rock solid part of my toolchain and I don’t hesitate to recommend using Devbox in production projects.
Devbox is a pretty powerful tool and I’ve barely scratched the surface. You can read more about some advanced features:
- 2023-11-19: Tweaked Nix in comparison table to reflect that “Per-project Isolation” and “Automatic Shell Activation (direnv)” are both achievable.
jetpack.io is committed to keeping devbox free and open source forever. ↩
When using Devbox you want to just find a package and what (potentially old) versions are available. Package versioning and simplicity is not something the other Nix package search engines focus on. This blog post explains in more detail. ↩
For example, if you want a very specific version of Node.js, you want to be able to install it by just using the semver version. For example, if I go to https://nodejs.org/en and see the LTS version is 20.9.0, I want to be able to install it with
devbox add firstname.lastname@example.org↩
Docker can be pretty reproducible, and there are lots of guides on the internet about how to build reproducible Dockerfiles, but it doesn’t by default encourage it. The
latesttag, for example, behaves completely differently in a Dockerfile and Devbox. In Devbox, it means “the latest available right now, but take a snapshot of what that is and re-use it in the future.” With Docker, it means “just #yolo grab whatever right now and feel free to grab something different in the future.” ↩