devops

Dagger CI/CD: Complete Guide for DevOps Engineers in 2026

Learn Dagger — the programmable CI/CD engine that runs pipelines in containers. Dagger Functions, multi-language SDK, caching, and GitHub Actions integration for portable CI/CD.

June 28, 2026·4 min read·
#dagger#cicd#devops#github-actions#containers

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

LanguageBest ForModule Support
GoBest performance, Go-native teamsFull
PythonData/ML pipelines, prototypingFull
TypeScriptNode.js ecosystem, frontend teamsFull

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

FeatureTraditional CI (YAML)Dagger
Pipeline portabilityLocked to one providerRuns anywhere
Local testingNot possibleFull pipeline on laptop
Cache sharingPlatform-specificOCI registry (universal)
Reusable componentsComposite actions (limited)Dagger Functions (typed)
CI migration costRewrite everythingChange 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.

#dagger#cicd#devops#github-actions#containers
D
DevToCashAuthor

Senior DevOps/SRE Engineer · 10+ years · Professional Trader (IDX, Crypto, US Equities)

I write about real infrastructure patterns and trading strategies I use in production and in live markets. No courses, no affiliate hype — just documentation of what actually works.

More about me →