⚠️ 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.
Intended Audience
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
Links
Devbox’s official site and getting started guide
What is Devbox
Devbox is a free and open source1 tool created by the company jetpack.io. According to the official site:
Devbox creates isolated, reproducible development environments that run anywhere. No Docker containers or Nix lang required.
A “development environment” is the environment you do any kind of software development in, whether you’re a data scientist using Python, a JavaScript-toting web developer, or a Rubyist wondering where all your friends have gone.
”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.
What Problems Does Devbox Solve?
Development Environment Rot
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.
Project Isolation
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.
Keeping Things Native and Local
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.
Share Your Development Environment with Your Collaborators
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.json
and 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.
How Does It Work?
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:
What’s Going on under the Covers?
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 apackage-lock.json
file, 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 likedevbox add nodejs@latest
, the exact version will be stored indevbox.lock
and re-used later anytime you enter the Devbox shell, so you will still always get the same version.The
packages
array indevbox.json
maps to the samepackages
key indevbox.lock
, in this casehello@latest
. In thedevbox.lock
file it stores what precise package that resolved to, in this case:github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#hello"
. That maps to a precise version ofhello
that 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 devbox.lock
:
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/store/2nsd0qwv17v6shhhmknyrpscjsa0p78r-hello-2.12.1/bin/hello
Wait, What is Nix?
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.
Getting Started Using Devbox
Browser Playground
Want to just mess around without committing to anything? The Devbox team has created a great browser-based playground for playing with Devbox which you can jump into here.
Installation and Initialization
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:
Searching for and Installing Packages
There are two ways to search for packages with Devbox. One is directly from the CLI:
The other is on the web. The Devbox team put together https://www.nixhub.io which makes searching for Nix packages as a Devbox user a little more intuitive3.
Workflow Tips
Making It Automatic with direnv
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!
Making It Automatic in VSCode
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.
Committing the Version
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.
Global 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 git
in every project, you candevbox global add git
and 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. puteval "$(devbox global shellenv)"
in~/.zshrc
). That will makegit
(or any otherdevbox global add
packages) 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:git
,difftastic
,nodejs
, andnodePackages.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 installgit
through 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.
Using in Collaborative Projects
Devbox relies on two files: devbox.json
and 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.gitignore
).
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.json
.
Comparison to Other Developer Environment Setups
Devbox | Devenv | Nix | Docker | Homebrew / Chocolatey | |
---|---|---|---|---|---|
Per-project Isolation | ✅ | ✅ | ✅ | ✅ | ❌ |
Use packages by semver5 | ✅ | ❌ | ❌ | ✅ | ✅ |
Automatic Shell Activation (direnv) | ✅ | ✅ | ✅ | ✅ | n/a |
Does not depend on VM | ✅ | ✅ | ✅ | ❌ | ✅ |
Reproducibility | ✅ | ✅ | ✅ | ⚠️ 6 | ❌ |
Share config in version control | ✅ | ✅ | ✅ | ✅ | ❌ |
Ease of use | ✅ | ❌ | ❌ | ❌ | ✅ |
Create Containers (share with production) | ✅ | ✅ | ✅ | ✅ | ❌ |
Windows support | ❌ 7 | ❌ | ❌ | ✅ | ✅ |
Global packages | ⚠️ 8 | ❌ (?) | ✅ | ❌ | ✅ |
Comparison to Homebrew/Chocolatey
- ✅ 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.
Comparison to Docker
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.
Comparison to Devenv
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 nodejs@20.9.0
I don’t want to think about which Nix package has that version of Node.js, which is another benefit over devenv.
Comparison to Nix
If you’re capable of using Nix to manage all the packages on your system, all shall love you and despair.
What Devbox is Bad At
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:
git
. - 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_DIR
environment 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.
Further Reading
Devbox is a pretty powerful tool and I’ve barely scratched the surface. You can read more about some advanced features:
Errata
- 2023-11-19: Tweaked Nix in comparison table to reflect that “Per-project Isolation” and “Automatic Shell Activation (direnv)” are both achievable.
Footnotes
-
jetpack.io is committed to keeping devbox free and open source forever. ↩
-
Nix starts being unapproachable the second you try to explain the name. I’m sorry. ↩
-
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. ↩
-
This was something that tripped me up. I thought seeing
@latest
indevbox.json
meant the version wasn’t locked-in, but that isn’t the case. ↩ -
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 nodejs@20.9.0
↩ -
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
latest
tag, 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.” ↩ -
Devbox, and anything that uses Nix under the covers, cannnot run natively on Windows. It does of course support running in WSL. ↩
-
See Global Packages for more information about the pros/cons of using Devbox for global packages. ↩