Nix for Everyone: Unleash Devbox for Simplified Development

Nix is amazing, but also very difficult to manage. It assumes that you learned its programming language and that you are willing to ignore frastration generated by horrible documentation. I think it is one of the most useful projects out there that cannot be used by anyone but those willing to dedicate their lives to Nix.

To be clear, in the Say Goodbye to Containers - Ephemeral Environments with Nix Shell video, I showed only the easy part. There’s much more to Nix than that and I’ll let you explore it on your own. Let me know in the comments how it went and whether you managed to figure it out, without going crazy.

The way I see it, Nix is like a cult. Those who managed to pass the initiation and joined its ranks swear by it, while the rest of the world finds it just not worth the trouble and sticks with their own religion.

Hence, we have a problem. We have a very useful project, one of the most interesting ones I’ve seen in a while, yet it has a very high barrier to entry.

I see two ways to reconciliate that. One would be for Nix to rewrite its programming language, to write documentation that is actually useful to mere mortals, and stop being a cult. Another solution is for Nix to be a project that powers other solutions.

There are many projects like that. That’s what happened with hypervisors, with kernels, and many others. That’s what’s happening right now with Kubernetes. We don’t work with hypervisors directly, but through services like AWS EC2. Most of us do not compile our own kernels but use distributions like Ubuntu. Many of us are creating abstractions or using ready-to-go abstractions on top of Kubernetes. When a technology is useful but complicated to use, handful of us use it directly while the majority of the rest use tools and abstractions built on top of it.

That’s what we’ll explore today. We’ll take a look at Devbox.

Devbox is a consistent shell for everyone. Unlike typical Shells like ZSH and Bash, Devbox shell enables us to define which tools should be available in a project by defining a JSON file typically stored in a Git repo. As a result, anyone working on a project can have all the tools required to work on that project without having to pollute their laptop. In a way, it is like an ephemeral operating system that can be spun up when needed and destroyed when not except that it is not spinning up virtual machines or Docker containers, even though, as we’ll see later, that is an option as well.

Now, if that description sounds familiar, it means that you already used Nix or that you watched Say Goodbye to Containers - Ephemeral Environments with Nix Shell. If you’re yelling at the screen thinking that you’re yelling at me for repeating what you already know, well… this is different.

Devbox is a wrapper around Nix. It makes Nix user friendly. It simplifies usage of Nix. It improves some of the features and it adds a few new ones.

Here’s a spoiler. Devbox is awesome and it is slowly becoming my go-to tool for setting up everything I need to work on a project. I’m already committed to Nix and Devbox makes it so much better and easier.

Now, to be clear, Devbox is not giving you environments to run your applications. Actually, it can, but that’s not its primary usage. You should continue running your apps as containers or whichever other way you’re doing it. Hence, if I say ephemeral environments, what I really mean, in the context of this video, is ephemeral environments for the tools you need to do whatever you’re doing when working on a project.

With that disclaimer out of the way… Let’s dive into it.

If you are familiar with Nix, you might think that what you’re about to see is the same. It’s not. It’s not. Continue watching.

Setup

git clone https://github.com/vfarcic/crossplane-sql

cd crossplane-sql

git checkout devbox
  • Open a second terminal session in the same directory.

Run Devbox

Here’s what I do when I want to work on a project.

I am inside one of my repositories, ready to start working. I do not have the tools I need to work on that project on my machine.

Here’s the proof.

which timoni

The output is as follows.

timoni not found

Among other tools, I need Timoni, and it is not there.

I could install it on my machine but that would mean that I’m polluting my laptop with stuff that I will probably never remove. More importantly, that project might need specific versions of tools so installing Timoni on my laptop might mean that the version I need for this project might not be the version I need in another.

This is the moment when I would probably consider using Docker containers which come with their own set of problem or I might spin up a VM with everything I need on my laptop or remotely but that also comes with some issues. What I really want is to work locally and in the same way I normally work. That means that I would want to start a Shell session, and that’s exactly what I’ll do but, instead of starting a ZSH terminal, that’s my favorite Shell, I’ll spin up a Devbox shell.

devbox shell

The output is as follows.

Ensuring packages are installed.
[1/9] kind@0.22.0
[1/9] kind@0.22.0: Success
[2/9] gh@2.44.1
[2/9] gh@2.44.1: Success
[3/9] kubectl@1.29.2
[3/9] kubectl@1.29.2: Success
[4/9] bat@0.24.0
[4/9] bat@0.24.0: Success
[5/9] timoni@0.17.0
[5/9] timoni@0.17.0: Success
[6/9] upbound@0.24.1
[6/9] upbound@0.24.1: Success
[7/9] yq-go@4.41.1
[7/9] yq-go@4.41.1: Success
[8/9] go-task@3.31.0
[8/9] go-task@3.31.0: Success
[9/9] kubernetes-helm@3.14.1
[9/9] kubernetes-helm@3.14.1: Success
✓ Computed the Devbox environment.
Starting a devbox shell...
───────┴─────────────────────────────────────
       │ File: README.md
───────┴─────────────────────────────────────
   1   │ ## Run tests
   2   │ 
   3   │ ```bash
   4   │ devbox shell
   5   │ 
   6   │ task cluster-create
   7   │ 
   8   │ task test-watch
   9   │ 
  10   │ # Stop watching with `ctrl+c`
  11   │ 
  12   │ task cluster-destroy
  13   │ 
  14   │ exit
  15   │ ```
  16   │ 
  17   │ ## Publish To Upbound
  18   │ 
  19   │ ```bash
  20   │ devbox shell
  21   │ 
  22   │ # Replace `[...]` with the Upbound Cloud account
  23   │ export UP_ACCOUNT=[...]
  24   │ 
  25   │ # Replace `[...]` with the Upbound Cloud token
  26   │ export UP_TOKEN=[...]
  27   │ 
  28   │ # Replace `[...]` with the version of the package (e.g., `v0.5.0`)
  29   │ export VERSION=[...]
  30   │ 
  31   │ task package-publish
  32   │ 
  33   │ exit
  34   │ ```
───────┴─────────────────────────────────────

We can see that Devbox installed all the tools I need and that it used specific versions of those tools. The important note here is that anyone else working with me on that project would get exactly the same environment no matter whether they are using MacOS, like I am, or Windows, or Linux.

More over, not only that I got the tools I need, but specific commands were executed. In this case, there is a single command that outputs README with, potentially, useful information to whomever is working on that project.

The important note here is that I could have used Devbox to sping up a remote environment instead. In that case, it would define a DevContainer which is closests to a standard we have for remote development environments. I could use it with GitHub Codespaces or Loft’s DevPod. I could have also used Devbox to create a Docker container as well.

I prefer local Shell environments, so that’s what I created. You might have a different preference, and that’s okay since Devbox is not limited to local Shells.

Now I should be able to work, and we can confirm that by, for example, confirming that Timoni is indeed installed.

timoni --version

The output is as follows.

timoni version 0.17.0

We can see that Timoni is working, but where is it?

Let’s see…

which timoni

The output is as follows.

/Users/viktorfarcic/code/crossplane-sql/.devbox/nix/profile/default/bin/timoni

We can see that it is inside one of the subdirectories in the project, meaning that Timoni and all other tools are local to the project. A different project will get different tools and even if there are the same, since the instances will be elsewhere, they could be different versions.

Let’s exit the Shell and take a closer look at how I defined which tools are needed for that project.

exit

Search and Define Devbox Packages

Here’s Devbox JSON I defined for this project.

cat devbox.json

The output is as follows.

{
  "packages": [
    "kind@0.22.0",
    "gh@2.44.1",
    "kubectl@1.29.2",
    "bat@0.24.0",
    "timoni@0.17.0",
    "upbound@0.24.1",
    "yq-go@4.41.1",
    "go-task@3.31.0",
    "kubernetes-helm@3.14.1"
  ],
  "shell": {
    "init_hook": [
      "bat README.md"
    ],
    "scripts": {
      "cluster-create": [ "task cluster-create" ],
      "cluster-destroy": [ "task cluster-destroy" ],
      "package-publish": [ "task package-publish" ],
      "test": [ "task test" ],
      "test-watch": [ "task test-watch" ]
    }
  }
}

It’s a simple one. That’s one of the benefits Devbox provides. That file is much easier to write and read than Nix equivalent. There’s no need to learn a new language. Instead, all I did was define packages with tools I need. There’s kind, gh (GitHub CLI), kubectl, and so on.

Further down is the optional shell section.

The init_hook specifies commands that will be executed every time we start a new Devbox Shell. You already saw it in action when I started a Shell. That’s how README was output.

Then there are scripts. Those are not executed when we start a new Shell but, rather, provide a convenient way to run scripts. You’ll see those in action soon.

Let’s get back to packages.

You might be wondering how I found out which packages and their versions are available. Some can be guessed easily like, for example, kind, while others not so much. Since there are multiple packages that contain helm in their name, the one I’m looking for, the one that provides templating for Kubernetes resources, is called kubernetes-helm. So, how can we find packages?

If you prefer working in a terminal, as I hope you do, there is a convenient devbox search command.

Let say that we’d like to add crossplane CLI to the mix. We can just search for it.

devbox search crossplane

The output is as follows.

Found 6+ results for "crossplane":

* crossplane  (0.5.8, 0.5.7)
* crossplane-cli  (1.15.0, 1.14.5, 1.14.3)
* python39Packages.crossplane  (0.5.8, 0.5.7)
* python310Packages.crossplane  (0.5.8, 0.5.7)
* python311Packages.crossplane  (0.5.8)
* python312Packages.crossplane  (0.5.8)

We can see thats there are multiple packages that contain the word crossplane. I suspect that the one I’m looking for is crossplane-cli, so let’s double-check whether that’s the case.

devbox info crossplane-cli

The output is as follows.

crossplane-cli 1.15.0
Utility to make using Crossplane easier

Hooray! That’s the one I need.

We could have retrieved the same information from search.nixos.org or nixhub.io pages as well. Still, for me, it is more convenient to search for packages through the devbox search command.

Next, I could add the package to devbox.json manually or execute devbox add followed with the package and optional version.

devbox add crossplane-cli@1.15.0

The output is as follows.

Info: Adding package "crossplane-cli@1.15.0" to devbox.json
[1/1] crossplane-cli@1.15.0
[1/1] crossplane-cli@1.15.0: Success

From now on, every time we start a new Devbox session, crossplane CLI will be available as well.

Truth be told, I don’t really need Crossplane CLI. The devbox.json file already contains everything I need and I used it only to demonstrate how we can find and add packages to a project. So, since that project does not need it, I’ll remove it.

devbox rm crossplane-cli

There’s a dark side of devbox search though. It does not work as expected.

Here’s an example.

Let’s say that we want to add task to the mix.

Task is great and if you’re not familiar with it, you might want to check out the Say Goodbye to Makefile - Use Taskfile to Manage Tasks in CI/CD Pipelines and Locally video.

I know that Task exists as a Nix package, so it should not be a problem to find it. Right?

devbox search task

The output is as follows.

Found 47+ results for "task":

* tasknc  (2020-12-17, 2017-05-15)
* tasksh  (1.2.0)
* taskell  (1.11.4, 1.11.3, 1.11.2, 1.11.1, 1.11.0, 1.10.1, 1.10.0, 1.9.4, 1.9.3, 1.7.3)
* taskflow  (3.6.0, 3.5.0, 3.4.0, 3.3.0)
* taskopen  (1.1.5, 1.1.4)
* tasktimer  (1.11.0, 1.9.4)
* taskserver  (1.1.0)
* taskjuggler  (3.7.2, 3.6.0)
* taskspooler  (1.0.1)
* taskwarrior  (2.6.2, 2.6.1, 2.6.0, 2.5.3, 2.5.2, 2.5.1)

Warning: Showing top 10 results and truncated versions. Use --show-all to show all.

None of those is Task I’m looking for. I know from other projects that the one I want is called go-task. Yet, Devbox did not find it. Does that mean that it does not have it in its database. Nope, that’s not it. Search is silly. I might be wrong but I think that it searches only for packages with names that start with the string instead of searching with all those that contains given keyword.

So, if I ask it to search for go-ta

devbox search go-ta

The output is as follows.

Found 4+ results for "go-ta":

* go-task  (3.34.1, 3.33.1, 3.32.0, 3.31.0, 3.29.1, 3.28.0, 3.27.1, 3.26.0, 3.25.0, 3.24.0)
* emacsPackages.go-tag  (20230111.651, 20180227.411)
* emacs27Packages.go-tag  (20180227.411)
* emacs28Packages.go-tag  (20180227.411)

Warning: Showing top 10 results and truncated versions. Use --show-all to show all.

go-task is there. It found it. That means that you need to know the name of the package, and that defies the purpose of a search. For example, if we look for helm, Kubernetes package manager,…

devbox search helm

The output is as follows.

Found 50+ results for "helm":

* helm  (0.9.0)
* helm-ls  (0.0.10, 0.0.9, 0.0.8, 0.0.7, 0.0.6, 0.0.5, 0.0.4, 0.0.3)
* helmfile  (0.161.0, 0.160.0, 0.159.0, 0.158.1, 0.158.0, 0.157.0, 0.156.0, 0.155.1, 0.155.0, 0.154.0)
* helmsman  (3.17.0, 3.16.4, 3.16.1, 3.16.0, 3.15.1, 3.15.0, 3.14.0, 3.13.1, 3.13.0, 3.8.1)
* helm-docs  (1.11.2, 1.11.1, 1.11.0, 1.10.0, 1.8.1, 1.7.0, 1.5.0)
* helmholtz 
* helm-dashboard  (1.3.3, 1.3.2, 1.3.1)
* helmfile-wrapped  (0.161.0, 0.160.0, 0.159.0, 0.158.1, 0.158.0, 0.157.0, 0.156.0, 0.155.1, 0.155.0)
* emacsPackages.helm  (20231114.1504, 20231108.1729, 20231027.1921, 20231017.449, 20230916.512, 20230825.2026, 20230806.1431, 20230610.741, 20230419.650, 20230401.441)
* rPackages.helminthR  (1.0.10, 1.0.9)

Warning: Showing top 10 results and truncated versions. Use --show-all to show all.

…it will not find it.

But, if we search for kubernetes,…

devbox search kubernetes

The output is as follows.

Found 35+ results for "kubernetes":

* kubernetes  (1.29.2, 1.28.4, 1.28.3, 1.28.2, 1.28.1, 1.27.4, 1.27.3, 1.27.2, 1.27.1, 1.27.0)
* kubernetes-helm  (3.14.1, 3.14.0, 3.13.3, 3.13.2, 3.13.1, 3.13.0, 3.12.2, 3.12.1, 3.12.0, 3.11.3)
* kubernetes-polaris  (8.5.5, 8.5.4, 8.5.3, 8.5.2, 8.5.1, 8.5.0, 8.4.0, 8.3.0, 8.2.4, 8.2.3)
* kubernetes-helm-wrapped  (3.14.1, 3.14.0, 3.13.3, 3.13.2, 3.13.1, 3.13.0, 3.12.2, 3.12.1, 3.12.0, 3.11.3)
* emacsPackages.kubernetes  (20221229.1519, 20220715.1717)
* kubernetes-code-generator  (0.25.4)
* kubernetes-metrics-server  (0.6.4, 0.6.3, 0.6.2)
* emacs27Packages.kubernetes  (20220331.1314, 20220213.1809, 20220111.1305)
* emacs28Packages.kubernetes  (20220715.1717, 20220331.1314)
* kubernetes-controller-tools  (0.13.0, 0.12.1, 0.12.0, 0.11.4, 0.11.3, 0.11.2, 0.11.1, 0.10.0, 0.9.2, 0.8.0)

Warning: Showing top 10 results and truncated versions. Use --show-all to show all.

kubernetes-helm is there.

That’s just silly. It often makes search useless.

Working in a Devbox Shell

Another potentially useful set of features are Devbox shell init hooks.

Let’s take another look at the JSON.

cat devbox.json

The output is as follows (truncated for brevity).

{
  ...
  "shell": {
    "init_hook": [
      "bat README.md"
    ],
    ...
  }
}

We already saw init_hook in action when we started a Shell session. It’s a set of commands that are executed every time we start a new Shell session.

In this case, there is only one command that outputs README.md.

Let’s see the result.

devbox shell

The output is as follows.

Starting a devbox shell...
───────┴─────────────────────────────────────
       │ File: README.md
───────┴─────────────────────────────────────
   1   │ ## Run tests
   2   │ 
   3   │ ```bash
   4   │ devbox shell
   5   │ 
   6   │ task cluster-create
   7   │ 
   8   │ task test-watch
   9   │ 
  10   │ # Stop watching with `ctrl+c`
  11   │ 
  12   │ task cluster-destroy
  13   │ 
  14   │ exit
  15   │ ```
  16   │ 
  17   │ ## Publish To Upbound
  18   │ 
  19   │ ```bash
  20   │ devbox shell
  21   │ 
  22   │ # Replace `[...]` with the Upbound Cloud account
  23   │ export UP_ACCOUNT=[...]
  24   │ 
  25   │ # Replace `[...]` with the Upbound Cloud token
  26   │ export UP_TOKEN=[...]
  27   │ 
  28   │ # Replace `[...]` with the version of the package (e.g., `v0.5.0`)
  29   │ export VERSION=[...]
  30   │ 
  31   │ task package-publish
  32   │ 
  33   │ exit
  34   │ ```
───────┴─────────────────────────────────────

There are two important notes here. First, Shell started almost instantly. It did not need to download packages since there were cached from the previous time we run Shell. The cache is local to the project, the directory giving us a unique and controlled environment related to that project.

The second note is that it executed init hook commands which, in this case, output README from the repo.

Since we’re already inside a Devbox shell, let me show you a glimpse of my workflow for that project.

That’s one of my Crossplane Configurations which, among other things, need to be tested. Typically, I would work in VS Code with code editor on one side and this terminal session in another. I’m not showing VS Code in this video, so you’ll have to use your imagination to visualize how it would really look like.

What matters is that I can follow the flow outlined in the README that was output when a new Devbox session is started.

First, I would setup a cluster and install everything I need. I’m using Task for that, so let’s run one of its targets.

task cluster-create

The output is as follows.

task: [helm-repo] helm repo add crossplane-stable https://charts.crossplane.io/stable
task: [cluster-create-kind] kind create cluster
task: [package-generate] pkl eval --format yaml pkl/aws.pkl --output-path package/aws.yaml
"crossplane-stable" already exists with the same configuration, skipping
task: [helm-repo] helm repo update
Hang tight while we grab the latest from your chart repositories...
task: [package-generate] pkl eval --format yaml pkl/google.pkl --output-path package/google.yaml
task: [package-generate] pkl eval --format yaml pkl/azure.pkl --output-path package/azure.yaml
...Successfully got an update from the "traefik" chart repository
...Successfully got an update from the "cilium" chart repository
...Successfully got an update from the "ddosify" chart repository
...Successfully got an update from the "openfunction" chart repository
...Successfully got an update from the "perfectscale" chart repository
...Successfully got an update from the "dapr" chart repository
...Successfully got an update from the "external-secrets" chart repository
...Successfully got an update from the "prometheus-community" chart repository
...Successfully got an update from the "crossplane-stable" chart repository
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.29.2) 🖼
 ✓ Preparing nodes 📦  
 ✓ Writing configuration 📜 
 ✓ Starting control-plane 🕹️ 
 ✓ Installing CNI 🔌 
 ✓ Installing StorageClass 💾 
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Thanks for using kind! 😊
task: [cluster-create] helm upgrade --install crossplane crossplane-stable/crossplane --namespace crossplane-system --create-namespace --wait
Release "crossplane" does not exist. Installing it now.
NAME: crossplane
LAST DEPLOYED: Sat Feb 24 12:12:41 2024
NAMESPACE: crossplane-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Release: crossplane

Chart Name: crossplane
Chart Description: Crossplane is an open source Kubernetes add-on that enables platform teams to assemble infrastructure from multiple vendors, and expose higher level self-service APIs for application teams to consume.
Chart Version: 1.15.0
Chart Application Version: 1.15.0

Kube Version: v1.29.2
task: [cluster-create] kubectl apply --filename providers/aws.yaml
provider.pkg.crossplane.io/provider-aws-rds created
provider.pkg.crossplane.io/provider-aws-ec2 created
task: [cluster-create] kubectl apply --filename providers/azure.yaml
provider.pkg.crossplane.io/provider-azure-dbforpostgresql created
task: [cluster-create] kubectl apply --filename providers/function-auto-ready.yaml
function.pkg.crossplane.io/crossplane-contrib-function-auto-ready created
task: [cluster-create] kubectl apply --filename providers/function-go-templating.yaml
function.pkg.crossplane.io/crossplane-contrib-function-go-templating created
task: [cluster-create] kubectl apply --filename providers/function-patch-and-transform.yaml
function.pkg.crossplane.io/crossplane-contrib-function-patch-and-transform created
task: [cluster-create] kubectl apply --filename providers/google.yaml
provider.pkg.crossplane.io/provider-gcp-sql created
task: [cluster-create] kubectl apply --filename providers/provider-kubernetes-incluster.yaml
serviceaccount/crossplane-provider-kubernetes created
clusterrolebinding.rbac.authorization.k8s.io/crossplane-provider-kubernetes created
Warning: ControllerConfig.pkg.crossplane.io/v1alpha1 is deprecated. Use DeploymentRuntimeConfig from pkg.crossplane.io/v1beta1 instead.
controllerconfig.pkg.crossplane.io/crossplane-provider-kubernetes created
provider.pkg.crossplane.io/crossplane-provider-kubernetes created
task: [cluster-create] kubectl apply --filename providers/sql.yaml
provider.pkg.crossplane.io/provider-sql created
task: [package-apply] kubectl apply --filename package/definition.yaml && sleep 1
compositeresourcedefinition.apiextensions.crossplane.io/sqls.devopstoolkitseries.com created
task: [package-apply] kubectl apply --filename package/aws.yaml && sleep 1
composition.apiextensions.crossplane.io/aws-postgresql created
task: [package-apply] kubectl apply --filename package/azure.yaml && sleep 1
composition.apiextensions.crossplane.io/azure-postgresql created
task: [package-apply] kubectl apply --filename package/google.yaml && sleep 1
composition.apiextensions.crossplane.io/google-postgresql created
task: [cluster-create] sleep 60
task: [cluster-create] kubectl wait --for=condition=healthy provider.pkg.crossplane.io --all --timeout=300s
provider.pkg.crossplane.io/crossplane-provider-kubernetes condition met
provider.pkg.crossplane.io/provider-aws-ec2 condition met
provider.pkg.crossplane.io/provider-aws-rds condition met
provider.pkg.crossplane.io/provider-azure-dbforpostgresql condition met
provider.pkg.crossplane.io/provider-gcp-sql condition met
provider.pkg.crossplane.io/provider-sql condition met
provider.pkg.crossplane.io/upbound-provider-family-aws condition met
provider.pkg.crossplane.io/upbound-provider-family-azure condition met
provider.pkg.crossplane.io/upbound-provider-family-gcp condition met
task: [cluster-create] kubectl wait --for=condition=healthy function.pkg.crossplane.io --all --timeout=300s
function.pkg.crossplane.io/crossplane-contrib-function-auto-ready condition met
function.pkg.crossplane.io/crossplane-contrib-function-go-templating condition met
function.pkg.crossplane.io/crossplane-contrib-function-patch-and-transform condition met

It will take a few minutes until local cluster is up and running and everything is installed, so let’s fast forward to the end of the process.

Now I would start working with tests running continuously in the background.

task test-watch

The output is as follows.

task: Started watching for tasks: test-watch
task: [package-generate] pkl eval --format yaml pkl/aws.pkl --output-path package/aws.yaml
task: [package-generate] pkl eval --format yaml pkl/google.pkl --output-path package/google.yaml
task: [package-generate] pkl eval --format yaml pkl/azure.pkl --output-path package/azure.yaml
task: Task "test-watch" is up to date

Whenever I change code or tests, the process would be reinitiated and a few moments later I’d get feedback whether all the tests passed. It’s test-driven development; the only way to develop something even if that something are Kubernetes resources.

I won’t bore you with the whole process since I already did that in the Say Goodbye to Makefile - Use Taskfile to Manage Tasks in CI/CD Pipelines and Locally video. I used a different project in that one, but the process is the same since the code is also related to a Crossplane Configuration.

The point is that I got an ephemeral environment for the tools I need and I can work without worrying whether I have whatever is needed installed on my machine.

Let me stop the watcher,…

FIXME: Press ctrl+c to stop watching for changes

…and show you another potentially annoying thing with Devbox.

Let me execute some silly command…

echo "Testing history"

…and exit the Shell.

exit

If I start typing the command I executed in the Devbox shell, ZSH autocomplete does not recognize it.

Start typing echo and you’ll see that auto-complete does not know about the previous echo command.

Devbox sessions are isolated from the Shell I have on my machine or, for that matter, other Devbox shells. In principle, that’s what we want but the negative outcome is that commands history is not shared so ZSH cannot use commands from Devbox sessions as suggestions or for auto-complete when in a different project or on the host. It’s a small annoyance, but annoyance nevertheless.

Next, let’s take a look at Devbox scripts which can be very useful when running in CI/CD pipelines.

Run Scripts with Devbox

Interactive Devbox shells are very useful when working on a project locally or remotely, but when it comes to CI/CD pipelines, it would be useful to have a mechanism to be able to define specific commands so that Devbox can start a Shell, run those commands, and exit the Shell automatically. We can accomplish just that with scripts which I already defined in the devbox.json, so let’s take another look at it.

cat devbox.json

The output is as follows (truncated for brevity).

{
  ...
  "shell": {
    ...
    "scripts": {
      "cluster-create":  ["task cluster-create"],
      "cluster-destroy": ["task cluster-destroy"],
      "package-publish": ["task package-publish"],
      "test":            ["task test"],
      "test-watch":      ["task test-watch"]
    }
  }
}

The scripts section is a map with script names as the key and an array of commands as the value.

We can also list all available scripts.

devbox run --list
Available scripts:
* test-watch
* cluster-create
* cluster-destroy
* package-publish
* test

Let’s run one of those. Since we already create a cluster, we might just as well want to destroy it.

Bear in mind that we are not inside a Devbox shell any more. The command that follows will create a new one with all the tools needed, run the command, and close the session.

Here it goes.

devbox run cluster-destroy

The output is as follows.

task: [cluster-destroy] kind delete cluster
Deleting cluster "kind" ...
Deleted nodes: ["kind-control-plane"]

That’s it. That’s all it takes to run something without having any of the required tools preinstalled. Devbox run command is probably what we should use in CI/CD pipelines, so let’s take a look at the GitHub Actions example I have in that repo.

cat .github/workflows/build.yaml

The output is as follows (truncated for brevity).

...
jobs:
  build-package:
    runs-on: ubuntu-latest
    steps:
      ...
      - name: Install Devbox
        uses: jetpack-io/devbox-install-action@v0.6.0
      - name: Test
        run: |
          devbox run test          
      - name: Build the package
        run: |
          export UP_TOKEN=${{ secrets.UP_TOKEN }}
          export UP_ACCOUNT=${{ secrets.UP_ACCOUNT }}
          export VERSION=0.8.${{ github.run_number }}
          devbox run package-publish          
      ...

Instead of figuring out all the tools that are required and specifying a bunch of container images or having an uber container image or installing all of those separately, this job installs devbox and simply runs some of the commands like, for example, devbox run test.

Using Devbox In DevContainers, Docker Containers, and DirEnv

There’s more though.

For those do not not want to always run Devbox shells locally but would like to benefit from Nix packages, Devbox can generate files to run it shells elsewhere.

Let’s take a look at what it can generate for us.

devbox generate --help

The output is as follows.

Generate supporting files for your project

Usage:
  devbox generate [command]

Aliases:
  generate, gen

Available Commands:
  devcontainer Generate Dockerfile and devcontainer.json files under .devcontainer/ directory
  direnv       Generate a .envrc file that integrates direnv with this devbox project
  dockerfile   Generate a Dockerfile that replicates devbox shell
  readme       Generate markdown readme file for this project

Flags:
  -c, --config string        path to directory containing a devbox.json config file
      --environment string   environment to use, when supported (e.g.secrets support dev, prod, preview.) (default "dev")
  -h, --help                 help for generate

Global Flags:
  -q, --quiet   suppresses logs

Use "devbox generate [command] --help" for more information about a command.

We can see that it can generate Dockerfile and devcontainer.json files to use with devcontainers like GitHub Codespaces or Loft’s DevPod.

It can also generate dockerfile that can be used to generate a container image with all the packages preinstalled.

Then there is the option to generate .envrc for direnv, or a readme for the project.

Finally, if we want to liberate some space, we can use Devbox to run nix-store garbage collector.

devbox run -- nix-store --gc

Devbox Pros and Cons

Devbox is awesoem. It democratizes Nix. It does not do anything that cannot be done with Nix alone. Instead, it makes Nix shell user friendly. It provides a wrapper that helps people who are not fully versted into Nix, those who did not choose to learn Nix language in depth.

I strongly recommend it. Even if you are a veteran Nix user who knows it inside out, and not many are, you’ll find Devbox a pleasure to work with.

Still, there are a couple of minor annoyances.

Cons:

  • Search
  • Command history

To begin with, search is bad. I cannot imagine how did anyone think that having a search for those who already know the exact name of a package is a good idea. It should look for packages that contain a keyword but, instead, it looks for those that start with the keyword. You’ll often find what you’re looking for but, in some cases, you’ll have to dig deeper to find a keyword combination that works. Or you’ll do what I do and search for packages on search.nixos.org to find the name you’re looking for and use it with devbox search since the official one does not provide the list of available versions. When search is concerned, both are bad, but for different reasons so you might need to combine them.

The second annoyance is that the command history is not global so I cannot use suggestions and auto-completions based on commands executed in one project in another or even on my host shell. That’s not a big deal unless you are, like me. My brain cannot remember all the commands and arguments so I tend to rely heavily on ZSH suggestions.

That’s it. Those are the only two negative things I have with Devbox.

Actually, there could be more but I’m scoping this review to Devbox being a wrapper of Nix so I’m not commenting on annoyances of Nix itself.

As for good things…

Pros

  • User friendly
  • Separation
  • Less workarounds
  • Open source

To begin with, unlike Nix itself, Devbox is user friendly. That’s its main value. It takes the power of Nix, and makes it useful to mere mortals; to all those who are not in the Nix cult.

Another positive thing is the clear separation between environments. Nix Shell is, by default, global. Packages downloaded for one project are available in all projects. While that might reduce initial download times, that also results in difficulties to keep specific versions of packages for specific projects. Devbox allows us to treat every project in isolation when packages are concerned.

Next, Devbox helps us avoid some silly workarounds. For example, I had to always add --run $SHELL argument with Nix so that it starts a Shell using my favorite flavor (ZSH) instead of whichever shell it comes with. Devbox, on the other hand, does not use its own or Nix’ shell but whichever is running on your system. Devbox is not a wrapper of Nix shell but, rather, a wrapper of Nix packages.

Finally, it is open source with Apache license.

Try it out. Let me know what you think.