Introduction
Traditional CI/CD pipelines are coupled to their runner. A GitHub Actions workflow only works on GitHub Actions. A Jenkins pipeline needs Jenkins. You cannot run them locally or migrate between CI providers without rewriting everything.
Dagger fixes this. Every pipeline step runs inside a container, making pipelines portable across any CI provider and fully testable on a developer laptop. This guide covers production Dagger patterns with real code examples.
What Makes Dagger Different
Dagger pipelines are programs — not YAML. You write them in Go, Python, or TypeScript, and Dagger executes each step in an isolated container. The same pipeline runs identically everywhere.
import dagger
async def build_and_test():
async with dagger.Connection() as client:
src = client.host().directory(".")
builder = (
client.container()
.from_("golang:1.22")
.with_directory("/src", src)
.with_workdir("/src")
.with_exec(["go", "build", "-o", "app", "./cmd/server"])
)
result = await builder.sync()
Run this with python pipeline.py on your laptop or in any CI provider.
Dagger Functions: Reusable Components
Dagger Functions wrap pipeline steps into reusable, typed modules:
type MyPipeline struct{}
func (m *MyPipeline) Build(
src *dagger.Directory,
goVersion string,
) *dagger.Container {
return dag.Container().
From("golang:" + goVersion).
WithDirectory("/src", src).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "app", "./cmd/server"})
}
Call it: dag.MyPipeline().Build(src, "1.22"). Publish Functions to the Daggerverse registry or keep them private.
CI Integration
Dagger runs as a single step in any CI system:
name: Dagger Pipeline
on: [push]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v6
with:
verb: call
args: build --src=.
Migrating CI providers changes only this thin wrapper — the pipeline logic stays the same.
Caching
Dagger caches every step by default. A build taking 90 seconds from scratch takes 3 seconds on a cache hit. Cache is exported to any OCI registry and shared across CI runners and developer laptops:
dagger call build --src=. --cache-to type=registry,ref=ghcr.io/org/cache:go-build
Container Image Publishing
Build and push images without Docker-in-Docker:
func (m *MyPipeline) Publish(
src *dagger.Directory,
registry, imageName, tag string,
) *dagger.Container {
return m.Build(src, "1.22").
WithEntrypoint([]string{"/app"}).
Publish(ctx, fmt.Sprintf("%s/%s:%s", registry, imageName, tag))
}
No Docker daemon, no privileged mode required in CI.
Local Debugging
The biggest productivity win is running pipelines locally before pushing:
dagger call build --src=.
Dagger prints container output to your terminal. Fix errors, re-run, push only when the pipeline passes locally. No more ten-minute CI feedback loops.
Service Containers for Integration Testing
Dagger spins up real services like PostgreSQL or Redis for integration tests and tears them down automatically. No Docker Compose needed. A Go Function starts a Postgres service container, runs tests against it, and cleans up — all within the pipeline.
Multi-Language SDK
| Language | Best For | Module Support |
|---|---|---|
| Go | Best performance, Go-native teams | Full |
| Python | Data/ML pipelines, prototyping | Full |
| TypeScript | Node.js ecosystem, frontend teams | Full |
All three share the same Dagger engine and can call each other across language boundaries.
Production Pipeline Patterns
Chain multiple Functions into a complete CI pipeline:
func (m *MyPipeline) CI(
src *dagger.Directory,
registry, imageName, tag string,
) *dagger.Container {
return m.Publish(
m.Test(m.Lint(src)),
registry, imageName, tag,
)
}
Each Function — Lint, Test, Build, Publish — is independently cacheable. If lint passes unchanged, Dagger reuses the cached output and skips execution.
Dagger vs Traditional CI
| Feature | Traditional CI (YAML) | Dagger |
|---|---|---|
| Pipeline portability | Locked to one provider | Runs anywhere |
| Local testing | Not possible | Full pipeline on laptop |
| Cache sharing | Platform-specific | OCI registry (universal) |
| Reusable components | Composite actions (limited) | Dagger Functions (typed) |
| CI migration cost | Rewrite everything | Change wrapper step |
Dagger is a CI/CD runtime, not a CI/CD replacement. Your existing CI provider still triggers pipelines. Dagger ensures those pipelines are portable, cacheable, and locally testable.
Why Dagger for Platform Engineering
Dagger aligns with the Internal Developer Platform philosophy. Platform teams provide Dagger Functions as a shared pipeline library. Application teams call dagger call deploy --env=production and trust that the platform team handles secrets, approvals, and compliance.
For Kubernetes deployments, pair Dagger's containerized CI with our Kubernetes security best practices guide to ensure your pipeline is as secure as your production cluster.
Dagger turns CI/CD from a configuration problem into a software engineering problem — and software engineering is what your team already does well.