How does Dagger compare to Bazel?
Bazel and Dagger work at different layers, so you do not have to choose only one. Bazel is a build and test tool. BUILD files declare fine-grained targets, and Bazel caches each action. Bazel can also run actions on remote machines. Dagger is a CI engine. It runs a project’s existing tools as checks in containers and caches each operation, the same way on a laptop and in CI.
Choose Bazel to build a large polyglot codebase incrementally, one action at a time. Choose Dagger to run the tools you already have the same way everywhere, with no new build files. Many Bazel teams can use both, because Dagger can run bazel test as one of its checks.
The fundamental difference
Bazel models the build itself. You declare targets in BUILD files with Starlark, and Bazel makes an action graph from them. In this graph, each compile, link, or test step has declared inputs and outputs. Bazel knows every action, so it rebuilds and retests only what a change affects. Its docs say that Bazel handles “builds with 100k+ source files”.
Dagger models the pipeline around your tools. A Dagger module is a package of functions that build and test a project. Its checks, such as tests and linters, run with dagger check. Official modules for Go, pytest, Jest, Vitest, ESLint, and other tools read the project’s native configuration, so you do not write build files. The Dagger engine runs each step in a container and caches the step by its inputs.
| Bazel | Dagger | |
|---|---|---|
| What you write | BUILD files and Starlark rules for each target |
Nothing for tools with an official module, or a small module for other tools |
| Unit of caching | One build action, such as one compile | One operation, such as one command in a container |
| Isolation | A sandbox for each action | A container for each step |
| Remote work | Remote cache and remote execution over gRPC, through a service you choose | Whole pipelines on a remote engine or on Dagger Cloud |
| CI | Runs inside the CI system you already have | Cloud Checks run dagger check on each push |
| Cost | Open source. Remote services cost what each vendor charges | Open-source engine. Dagger Cloud has usage pricing |
Adoption
Bazel needs more work at the start than Dagger. Each package needs a BUILD file, and each language needs a rule set, such as the Go, Java, Python, or JavaScript rules. The Bazel docs recommend that you improve hermeticity before you migrate. Common problems include system binaries that differ between hosts and build steps that write to the source tree. For a large codebase, the result is correct incremental builds across all its languages.
Dagger starts from the files the project already has. Run dagger init, then run dagger module recommend. Dagger looks for files such as go.mod or pyproject.toml and recommends the official modules that match them (Quickstart). Your tests and build commands stay the same. If a tool has no official module, you write a small module. You can write it in Dang, Dagger’s native module language (Dang SDK), or in Go, Python, TypeScript, or another SDK language.
Caching
Both tools cache by content, but at different levels of detail.
Bazel caches each action. A remote cache holds an action cache and a content-addressable store, so a team and its CI can share outputs. The --disk_cache flag keeps a cache on the local file system. You choose, set up, and maintain the backend for the remote cache yourself, such as bazel-remote, nginx, or Google Cloud Storage. Cache hits across machines depend on hermetic actions. The Bazel docs give a procedure to debug cache misses with execution logs.
Dagger caches each operation in a pipeline by its inputs. An example of an operation is one command in a container. Dagger also has cache volumes. A cache volume is a directory that persists across runs, for example for a package manager cache. A Dang module reads project files only when it uses them (workspace inputs). Two rules decide whether a step runs again:
- If the inputs of a step did not change, Dagger skips the step.
- If an input of a step changed, the step runs again in full.
Incremental work inside a step comes from the tool in that step, for example from Bazel’s own cache in a cache volume.
Remote execution
Bazel distributes single actions. Remote execution sends build and test actions to many machines over an open gRPC protocol. The Bazel docs say this gives “faster build and test execution through scaling of nodes available for parallel actions”. Bazel lists self-hosted and commercial services, such as Buildbarn, Buildfarm, BuildBuddy, EngFlow, and NativeLink. Your rules must meet the requirements for remote execution.
Dagger runs a whole pipeline on a remote engine. Set --engine to an SSH host, a Kubernetes pod, or cloud for a managed Cloud Engine (changelog). Dagger streams local files, secrets, and your terminal to the engine when it needs them. Dagger also splits large test suites across engines (test splitting).
Local and CI runs
Bazel gives the same results on a laptop and in CI when both use the same Bazel version, flags, and environment. The usual tools for this are a .bazelversion file, which Bazelisk reads, and a --config=ci section in .bazelrc. Bazel’s sandbox limits which inputs an action sees. But the Bazel docs say: “Sandboxing doesn’t hide the host environment in any way.” Toolchains that Bazel does not manage can be different on a Mac laptop and on a Linux runner. A CI system must still call Bazel, so part of the pipeline stays in the configuration of that system.
With Dagger, the checks run in containers that the module defines, so the environment is part of the pipeline. dagger check runs the same checks on a laptop and in CI (Checking your code). Cloud Checks connect a GitHub repository to Dagger Cloud. After that, every push and pull request runs your checks on Cloud Engines, with no CI workflow file. When a check fails, dagger check -i opens a shell in the state where the command failed (changelog).
Languages
Bazel supports many languages through rule sets. The recommended rules include C/C++, Go, Java, Python, Rust, Scala, Android, and Protocol Buffers. Bazel can also build for several platforms from one project, such as desktop, server, and mobile.
Dagger runs any tool that runs in a Linux container. The official modules cover Go, JavaScript and TypeScript (Jest, Vitest, Playwright, ESLint, Prettier, Biome, Deno), Python (pytest), shell scripts, PowerShell, and Helm (module catalog). For other tools, a module runs the tool’s own command in a container.
Pricing
Bazel is open source. A remote cache or remote execution service costs what its vendor charges, or what it costs you to run it yourself. Check the pricing page of each vendor.
The Dagger Engine is open source, and it costs nothing on your own machine or server. Dagger Cloud bills the CPU, memory, and data that each pipeline uses. The monthly minimum is $20 for individuals and $200 for teams. An operation that could come from the cache costs 5× less (pricing).
Run Bazel inside Dagger
You can keep Bazel as the build system and use Dagger to run it. Bazel then builds incrementally inside a container. Dagger supplies the same container, cache, and checks on a laptop and in Cloud Checks. The module below uses the Bazel container image that the Bazel project publishes.
First, set up the workspace and create the project’s dev module. The dev module holds the parts of the pipeline that are specific to the project:
dagger init
dagger module install dagger.io/sdk/dang
dagger module init dang -y
With no --name, dagger module init creates a module named <project>-dev and installs it in dagger.toml. This guide’s scratch project is named work, so its module is work-dev.
Replace the contents of .dagger/modules/<project>-dev/main.dang with this module. The type name WorkDev stands for the name that dagger module init generated:
"""
Run the Bazel build and tests in a container.
"""
type WorkDev {
"""
The workspace, without Bazel's output symlinks.
"""
pub source: Directory!
new(ws: Workspace!) {
self.source = ws.directory("/", exclude: ["bazel-*", ".dagger", ".git"])
self
}
"""
A container with Bazel, the source, and a cache volume for Bazel's output.
"""
pub bazel: Container! {
container
.from("gcr.io/bazel-public/bazel:7.4.1")
.withMountedCache(
"/home/ubuntu/.cache/bazel",
cacheVolume("bazel-output"),
sharing: CacheSharingMode.LOCKED,
owner: "ubuntu",
)
.withDirectory("/src", source, owner: "ubuntu")
.withWorkdir("/src")
}
"""
Run every Bazel test target with the ci config from .bazelrc.
"""
pub test: Void @check {
bazel.withExec(["bazel", "test", "--config=ci", "//..."]).sync
null
}
}
The cache volume keeps Bazel’s output directory between runs, so Bazel’s own action cache continues to work. Set the image tag to the version in your .bazelversion file. The module copies .bazelrc with the source, so --config=ci applies your CI flags. The flags for a Bazel remote cache are Bazel flags, so they also go in .bazelrc.
List the checks:
dagger check -l
MODULE CHECK DESCRIPTION
dang-sdk stale staleness check: re-generate modules and clients managed by the Dang SDK
work-dev test Run every Bazel test target with the ci config from .bazelrc.
Run them:
dagger check
== CHECKS == ✔ 2 passed
✔ dag://?check=dang-sdk/generate/stale 1.2s OK
✔ dag://?check=test 0.0s OK
To run one Bazel target in the same container, use dagger shell:
dagger shell bazel -c 'bazel test //greet:greet_test'
What the two caches did in one run
The two caches have different jobs, and one example run on one machine shows each of them. The scratch project had two sh_test targets:
- The first run.
dagger checktook 16.8 seconds, and Bazel ran both tests. - A run again with no changes. It took 1.3 seconds. The inputs were the same, so Dagger skipped the Bazel step.
- A one-line change to one test file. It took 17.7 seconds. An input changed, so Dagger ran the Bazel step again. In that step, Bazel showed
(cached) PASSEDfor the test that did not change, and it ran only the other test.
When to choose Bazel
Choose Bazel when the build itself is the slow part. An example is a large monorepo where a change must rebuild only the affected targets. Another example is C++ or Java code with long compile steps. Choose Bazel also when you need to send single actions to a cluster with remote execution. Bazel can find the targets that depend on a change with rdeps in bazel query. Dagger’s feature to run only the affected checks is in the Coming soon section, so it is not released yet.
When to choose Dagger
Choose Dagger when the project builds well with its own tools, such as go test, pytest, or Vitest. The work is then in the CI around those tools. Dagger runs those tools as checks with no BUILD files, in the same containers on a laptop and in CI. Cloud Checks replace the CI workflow file. Choose Dagger also when the pipeline needs more than a build. Examples are services for integration tests, a shell in the state where a check failed, and remote engines that developers, agents, and CI share.
If you already use Bazel, you can use both. Keep BUILD files for incremental builds. Use Dagger to run bazel test with one container, one cache, and one command everywhere.
Bottom line: Bazel is a build system. It caches and distributes single build actions that BUILD files define, and it is best for large polyglot monorepos. Dagger is a CI engine. It runs your existing tools as cached container checks, the same way on a laptop and in Cloud Checks, with no build files. You can use them together: a Dagger module can run Bazel in a container, with a cache volume for Bazel’s output.
Related guides
- What are the alternatives to GitHub Actions, from faster runners to portable pipelines?: where Dagger fits among other CI options.
- How do I migrate from GitHub Actions to Dagger?: how to move a workflow to Dagger checks, including a workflow that calls Bazel.
- Which development tools can Dagger replace or consolidate?: what Dagger can replace and what stays in place.