Dagger vs. bob

Andrei Boar
benchkram
Published in
7 min readAug 23, 2022

--

Intro

Today, it’s time to check out another build tool and see how it compares to bob. So let’s have a look at Dagger, a tool created by the guys behind Docker.

Dagger promotes itself as a “portable devkit for CI/CD pipelines” and tries to solve the same problems as bob: fast builds, parity between dev and CI environments, ability to run your pipelines locally, etc.

The most significant difference between them is the fact that Dagger chose CUE as their configuration language. In the following article, I will explore how this decision made Dagger what it is and how it compares to bob.

I’m currently working at benchkram, the company behind bob, so I’ll try to be as less biased as possible.

Getting started

For this post, we’ll use Dagger’s Todo App Example.

You should have Dagger installed and bob to run their build commands.

Clone the Todo app and cd into it:

$ git clone https://github.com/zuzuleinen/todoapp 
$ cd todoapp

Other dependencies

bob requires Nix which is used to install your preferred dependencies (compiler-tool-chains & tools) for your tasks.

Dagger’s only dependency is Buildkit. If you have Docker installed, you don’t need to install Buildkit because on start Dagger will spawn a Buildkit container for you.

If you plan to follow along then you need to install these dependencies.

Configuration files quick preview

For the Todo app example this is how their Dagger plan looks:

And this is how the corresponding bob.yaml file looks like:

If you place this bob.yaml file in the todoapp directory, you will be able to run bob build as well as dagger do buildcommands to build the project.

Build time benchmarks

I used the time command to see how long each build takes in seconds.

First I ran the entire build from scratch with an empty docker registry and empty /nix/store, then a second build without doing any changes and third I did a small change in src/components/Form.js and triggered a rebuild.

On the first build, bob takes longer because Nix downloaded all the required store paths from the binary cache, however on the subsequent builds bob takes less time because it doesn't have to spawn any Docker containers as Dagger does.

The system I ran the benchmarks on:
Ubuntu 22.04.1 LTS 64-bit
11th Gen Intel® Core™ i7–1165G7 @ 2.80GHz × 8
Memory: 32,0 GiB
Speed internet: 83.96 Mbps
dagger: 0.2.26 (cc4e36595)
bob: 0.5.0-rc

CUE vs YAML

Dagger’s configuration is written in CUE which might come with a steeper learning curve. At least this is how I felt while reading the docs: I always struggled to keep in mind where CUE ends and where Dagger begins. However, it might pay off to get familiar with CUE due to its declared benefits.

With a YAML type configuration, you can only declare data in the YAML format.
Since CUE is a configuration language, you can use it to declare, but also operate on that data, specify constraints for your values, and so on. Things like if statements, for loops, comprehensions, and string interpolation, among others are just not possible in a YAML-based config without the use of a separate process for execution.

In a podcast from last year, the creators of Dagger were explaining that what Dagger is aiming at by using CUE is to “make the problem of application delivery actual programmable”. With YAML you will always have this gap between the YAML config and the processes you are starting in your tasks. I think by choosing CUE they are trying to close that gap.

Actions vs Task

In Dagger, everything is an action. Actions can reuse other actions definition allowing you to create composable actions. Actions cannot be used independently: they have to be organized in a plan. Bellow, you have 2 actions from the Todo app plan:

In bob, you have tasks organized in a bob.yamlfile. For the Todo app, the build task will look like this:

The cmd part will be executed in its own shell, so you can use all the commands used in a typical Unix shell.

bob’s YAML format might appeal to users who like simple shell commands which are available in most CI tools, while Dagger’s CUE configuration provides a more programmatic way of defining build actions.

Piping between tasks

In the above Dagger example, we see how the deploy action is using the output of build action with contents: actions.build.output allowing you to combine actions by creating pipes between them in a declarative style:

bob is more imperative in this regard. To have the same deploy command, you need a task that depends on another task using the dependson node, and then explicitly use the result of that task(./build in our case):

The benefit that CUE brings here is that when you have composable actions the relationship between them is more explicit than in a YAML format where you would have to figure out what each cmd does.

Build isolation and dependencies

Dagger is going in the direction of isolated builds by using containers under the hood. For example, we see that the build action is using the #Script definition:

Checking out the yarn package from Dagger Universe, we see that #Script it’s embedding a #Command which will run a container with bash, yarn and git installed:

When you run a bob command, that command is running in a sandboxed shell: your environment is cleared and all dependencies are installed by Nix and added by bob to the command’s $PATH.

What you add to the dependencies node will be automatically installed by Nix from the Nix remote store.

Below yarn is installed for you automatically without the need for Docker containers:

You can find out more here about how Bob is using Nix for package management.

Managing 2 tasks with different versions of the same package is quite convenient with bob because you don’t have to create separate containers. Bellow, there are 2 tasks that use Java 8 and 11:

Running bob build java8and bob build java11 will show you 2 different versions:

$ bob build java8   
Building nix dependencies...
Succeeded building nix dependencies
Running task java8 with 0 dependencies
java8 running task...
java8 openjdk version "1.8.0_322"
java8 OpenJDK Runtime Environment (build 1.8.0_322-ga)
java8 OpenJDK 64-Bit Server VM (build 25.322-bga, mixed mode)
java8 ...done
● ● ● ●
Ran 1 tasks in 90ms
java8 ✔ (90ms)
$ bob build java11
Building nix dependencies...
Succeeded building nix dependencies
Running task java11 with 0 dependencies
java11 running task...
java11 openjdk version "11.0.15" 2022-04-19
java11 OpenJDK Runtime Environment (build 11.0.15+0-adhoc..source)
java11 OpenJDK 64-Bit Server VM (build 11.0.15+0-adhoc..source, mixed mode)
java11 ...done
● ● ● ●
Ran 1 tasks in 120ms
java11 ✔ (120ms)

I think in this regard is much easier to manage your dependencies with bob because you are not relying on docker containers. Different sets of packages are much easier to combine when you just have to list them in the Bobfile as opposed to creating containers with Dagger.

The only problem I see for both of them is that if there is no package in the Nix store or the Dagger universe, you would have to write a config for them yourself. While the Dagger Universe is still new and growing, the Nix remote store has over 80,000 software packages pushed by maintainers.

Environment variables

I said that CUE allows you to add type constraints to fields. An excellent use case for this is how Dagger loads the environment variables from your machine. It can either load them as plain strings or as secrets depending on the type added:

Currently, bob can only load them as strings:

If you have secrets that you don’t want to add to the Bobfile, you can still use the --env flag on the bob build command:

$ bob build deploy --env NETLIFY_AUTH_TOKEN=your-netlify-token

Dagger Universe

Because CUE is a programming language, you can reuse actions written by others.

Dagger created Dagger Universe — a catalog of reusable CUE packages, curated by Dagger but possibly authored by third parties.
The import statement is similar to Go’s import, a language from which CUE took inspiration:

One of the reasons it’s beneficial to import other packages is the fact that they come with their own build environment. Take a look at the yarn and netlify actions. Each of them uses an alpine-based docker image with some additional installation steps. Those steps are different for each package and a docker image needs to be created on the first run on your system.

Bob has easily composable build environments without the need to install dependencies in a docker image. This relieves you from maintaining build environments for different programming languages and tools. Therefore publicly shareable packages will not be as important as Dagger Universe is for Dagger.

Closing thoughts

Dagger had its official launch 4 months ago and bob is under active development and planning to launch soon. They are both great products and there’s more to them, here I wanted to show the differences between their configuration files and how they manage dependencies.

If you don’t want the overhead of building in Docker you can go with bob, or if you prefer a more declarative way to define your pipelines you can go with Dagger. I think they each have their benefits.

If you want to find out more, check out their websites: Dagger | bob.build

--

--