A while ago I became aware of Nix, NixOS and people who actually use it. I was intrigued by the proposition: configure your whole system declaratively so you can avoid configuration drift and combat issues with reproducibility.
I tried it out and found that Nix is not straightforward to learn. Also, the documentation is gruesome. I decided that knowing Nix and applying it might be nice but I just don’t want to learn it right now.
However, there are tools which build on Nix to deliver something I really want: reproducible developer environments. I probably don’t need to explain why one might want that but I’ll give a quick overview of the benefits:
- Automated installation of dependencies, runtimes, tools
- Control over versions of used tools
- Declaration of environment variables
- Declaration of shell hooks
- Easy definition of specific shell commands
- Integration with
direnv
- Access to the extensive Nix packages repo
Also, Nix has a nice advantage over devcontainers, namely that it’s much easier to integrate your dev environment with an IDE that gets to use what’s inside. Any tool that works this way has Nix as a dependency. Since it’s, in principle, a package manager, you can install it on a variety of systems, like my own Fedora install. I prefer to use the Determinate Systems installer:
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
This sets everything up and also comes with the possibility to uninstall everything again
by using the appropriate option of the installer located in /nix/nix-installer
after
installation. This also sets up support for the new nix
command syntax and flakes. If
you don’t know what that is, don’t worry, it just means you don’t have to mess with config
files yourself.
There are two tools that I tried and used for dev environments, devbox
and devenv
.
devbox
devbox
is written in Go and is configured via JSON. It’s a relatively simple tool
that’s a good fit, if you want something straightforward without ever having to
write a single line of Nix. You install it via a one-liner:
curl -fsSL https://get.jetpack.io/devbox | bash
or, alternatively, you can just download the binary and place it somewhere appropriate.
To start a new dev environment you type
devbox init
which creates a new devbox.json
file
in your current directory. If you’re like me and want integration with direnv
so
everything’s loaded up when you enter the directory, you can then do
devbox generate direnv
This will create an appropriate .envrc
file and load it.
A devbox.json
file looks something like this:
{
"packages": [
"go",
],
"env": {
"FOO": "Bar",
},
"shell": {
"init_hook": [
"go version"
],
"scripts": {
"hello": [
"echo \"Hello World!\""
]
}
}
}
Note that trailing commas are supported. Thank God. This includes your packages, environment variables, shell hooks, shell scripts and more (if you want it to). There’s a lot more you can do.
devbox
also comes with a few handy commands.
To add new packages to the environment you can just do
devbox add $PACKAGE_NAME[@PACKAGE_VERSION]
The version is optional, if omitted the latest version will be selected. If you don’t know the name of the package or which versions are available, you can search for them with
devbox search $PACKAGE_NAME
This version control is a gem because targeting a specific pacakge version with Nix can be quite
tedious because it usually requires declaring a specific nixpkgs
version as input but devbox
kindly does this for you.
You can update the packages with
devbox update
and a lot more. The developers also provide template files for projects containing common languages or tools which can be found here.
The only reason why I stopped using devbox
myself is that the provided template doesn’t
really play nice with Rust.
devenv
devenv
is a project written in Nix itself and provides an abstraction for the
end user such that the configuration file is a very straightforward Nix file that
still provides a lot of possibilities for customization, if you desire (and know Nix).
Note that there are several ways to install devenv
since it’s built with Nix. I chose
the option utilizing flakes without making use of the declarative features.
To get started you need to install Cachix:
nix profile install nixpkgs#cachix
cachix use devenv
and then install devenv
:
nix profile install --accept-flake-config tarball+https://install.devenv.sh/latest
That’s it. To initialize a dev environment you then do
devenv init
This creates several files, a devenv.nix
which is your primary config file,
a devenv.lock
which is the lockfile pinning your dependencies, a devenv.yaml
which contains (mostly) the used inputs and an .envrc
for direnv integration.
A devenv.nix
file looks something like this:
{ pkgs, ... }:
{
# https://devenv.sh/basics/
env.AWS_PROFILE = "123456789";
# https://devenv.sh/packages/
packages = [ pkgs.nest-cli ];
# https://devenv.sh/scripts/
scripts.tests.exec = "npm run test";
scripts.e2e.exec = "npm run test:e2e";
scripts.lint.exec = "npm run lint";
scripts.docker-build.exec = "docker build -t invoice-upload .";
scripts.docker-run.exec = "./test/test_run.sh";
# https://devenv.sh/languages/
languages = {
javascript = {
enable = true;
package = pkgs.nodejs_18;
npm.install.enable = true;
};
typescript.enable = true;
};
# Enabling dotenv is helpful for bootstrapping the environment for local testing
# but will interfere when interacting with the remote git repository
dotenv.enable = false;
dotenv.disableHint = true;
# https://devenv.sh/pre-commit-hooks/
# pre-commit.hooks.shellcheck.enable = true;
# https://devenv.sh/processes/
processes.test-containers.exec = "docker-compose -f test/docker-compose_local.yml up";
# See full reference at https://devenv.sh/reference/options/
}
You can see quite similar features as compared to devbox
, just in another format.
We still declare packages, environment variables, scripts, shell hooks but devenv
also exposes nice features like languages
which makes setting up specific languages
quite straightforward because sometimes you need to do more than just installing
a package or two (like with Rust). There’s also support for dotenv
, pre-commit hooks,
long-running processes and more.
You can find a list of supported languages and all the available options here.
There’s also a list of supported services such as web servers or databases (see here).
All of this is quite comprehensive and well-made and I have yet to encounter any
real issue. The biggest downside to the whole thing is that it’s not
straightforward to choose a specific package version. By default the nixpkgs
input
pulls from the unstable channel. If you want something else than what’s available
there, you need to add another input in the devenv.yaml
. Courtesy of the people
from jetpack (who make devbox
) there are resources like this
where you can look up the nixpkgs
version that corresponds to a specific package version
you might need. This is all possible but quite annoying. Fortunately, I haven’t needed
to do this before but since one of the whole points of this setup is reproducibility
it’s quite important.
Conclusion
Both of these tools are great and highly useful. Personally, I turned to devenv because I find it a bit more powerful and I prefer the Nix format over JSON for configuration. Of course this is highly subjective as both projects are under active development and changes will (or already have) happen(ed).
What I most love about these tools is that I can have a fairly simple setup for all
my projects with all of the tools I need, declare everything that’s necessary to work
on something and am able to just cd
into a directory and then have everything magically
come into being. I can then just start up an editor or IDE and get going.
Granted,
depending on what you’re working with, you wil probably have to point the IDE at the
path where a runtime or executable lives within the Nix profile in your dev environment
but that’s a minor nuisance since you only have to do it once.
Also, the config files can be checked in to version control so I don’t have to worry about having to set everything up from scratch again. How many times have you set up a dev environment of any kind with env variables, packages, config changes and whatnot only to forget what exactly you did? And then, six months later something changes, you have to reinstall, you get a new machine and you have no idea what you did to set up and have to work your way through it again. Sounds familiar? It definitely does to me!
Happy coding!