Dagger: CI/CD Pipelines as Code That Run Anywhere
Stop writing YAML you can only test by pushing to a branch

The single most demoralising thing about CI is the feedback loop. You edit a YAML file, push, wait three minutes for a runner to spin up, watch it fail on a typo, fix the typo, push again. Repeat until either the pipeline goes green or you go home. The pipeline only exists inside the CI provider, so the only way to run it is to use the CI provider. Dagger’s whole pitch is that this is daft, and they’re right.
1 What Dagger actually is
Dagger is a portable execution engine for pipelines. You describe your build, test and deploy steps as code — in Go, Python, TypeScript, or via a shell-like CLI — and Dagger compiles that into a DAG of containerised operations executed by BuildKit under the hood. Because every step runs in a container, the pipeline produces the same result on your laptop as it does on a GitHub Actions runner, a GitLab job, or a Jenkins box. Your CI provider stops being the thing that defines the pipeline and becomes a dumb trigger that runs dagger call.
Crucially it’s content-addressed and aggressively cached, so unchanged steps don’t re-run. Run it twice and the second run is mostly cache hits.
2 A pipeline you can actually run locally
Here’s a function written in Go using the Dagger SDK. It builds a binary, runs the tests, and returns the test output — all inside containers Dagger manages.
package main
import "context"
type Ci struct{}
// Test runs the Go test suite against the source directory.
func (m *Ci) Test(ctx context.Context, src *Directory) (string, error) {
return dag.Container().
From("golang:1.24").
WithDirectory("/app", src).
WithWorkdir("/app").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod")).
WithExec([]string{"go", "test", "./...", "-v"}).
Stdout(ctx)
}Now the part that matters. You invoke that from your terminal:
$ dagger call test --src=.
=== RUN TestParseConfig
--- PASS: TestParseConfig (0.00s)
=== RUN TestRoundTrip
--- PASS: TestRoundTrip (0.02s)
PASS
ok example.com/app 0.231s
No push. No waiting for a runner. The exact same code path your CI will execute, running on your machine, in seconds. When it’s green locally, it’s green in CI — because it’s the same engine.
3 Wiring it into your provider
Your CI config shrinks to almost nothing. It installs the Dagger CLI and calls the function. Here’s the whole GitHub Actions job:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v8
with:
version: "latest"
call: test --src=.That’s it. The logic lives in ci/main.go, version-controlled and refactorable like any other code. Want to switch from GitHub to GitLab next year? You rewrite ten lines of trigger config and your actual pipeline is untouched. That portability is the headline benefit, and it’s real — I’ve moved a project between two providers and the migration was an afternoon, not a fortnight.
4 The catch, because there’s always a catch
Dagger is not free of friction. You’re now running a BuildKit engine — there’s a persistent dagger-engine container, and on a fresh runner the first invocation pays a startup tax before caching kicks in. Writing pipelines as Go or Python is more powerful than YAML but also more rope to hang yourself with; a junior who could copy-paste a GitHub Actions snippet now has to read SDK code. And the ecosystem of pre-built modules (the Daggerverse) is growing but still patchier than the mature marketplace of established CI providers.
There’s also a conceptual hump. People expect a pipeline to be a list of steps; Dagger wants you to think in terms of composable functions returning containers and directories. A function takes a directory or container in and returns a new one out, and you chain them — which is lovely once it lands and bewildering before it does. Once it clicks it’s genuinely elegant, but the first week feels like learning to cook by being handed a chemistry set. Budget time for the team to actually absorb the model rather than copy-pasting examples and hoping; the people who bounce off Dagger are almost always the ones who never made that mental shift.
5 Verdict
Dagger is worth it if your pipeline is complex enough that the push-wait-fail loop is costing you real hours, or if you’re allergic to vendor lock-in. The local-execution story alone justifies it for anyone debugging a gnarly multi-stage build. If your CI is “run the tests, build a container, push it” and it already works, Dagger is a solution to a problem you don’t have — leave it. But for teams maintaining serious, branching, multi-environment pipelines, being able to run the whole thing on your laptop is the kind of quality-of-life upgrade you don’t give back. I’d reach for it on any new project of meaningful size.




