Dagger: Programmable CI/CD with 15K+ Stars — Comparison vs GitHub Actions, GitLab CI in 2026

Dagger is a programmable CI/CD engine that runs pipelines in containers. Compatible with Docker, Go, Python, TypeScript. Covers Dagger setup, tutorial, vs GitHub Actions, and production hardening.

  • ⭐ 15829
  • Apache-2.0
  • Updated 2026-05-19

{{< resource-info >}}

Introduction #

The average developer pushes 3.2 commits per day and waits 12 minutes for each CI run to finish. Multiply that across a team of 20 engineers, and you are looking at 4 hours of collective idle time daily — all because CI/CD pipelines cannot be tested locally, are locked to vendor-specific YAML syntax, and fail unpredictably between environments.

Dagger, created by Solomon Hykes (co-founder of Docker), takes a different approach: it treats CI/CD pipelines as regular code. Instead of wrestling with GitHub Actions YAML or GitLab CI DSL, you write your automation in Go, Python, or TypeScript. The pipeline runs inside containers using the same BuildKit engine that powers Docker, giving you identical behavior on your laptop and in production.

With 15,829 GitHub stars, 878 forks, and 304 contributors as of May 2026, Dagger has established itself as a serious alternative to traditional CI/CD. This Dagger tutorial walks through a complete Dagger setup, compares it head-to-head against GitHub Actions, GitLab CI, and Jenkins, and covers production hardening patterns you can deploy today.

Dagger CI/CD Engine Architecture

What Is Dagger? #

A programmable CI/CD engine that executes automation pipelines inside OCI containers, letting developers define build, test, and deploy logic in general-purpose programming languages instead of YAML or proprietary DSLs.

The Core Value Proposition #

Dagger embodies the CI/CD as code (also called cicd as code) philosophy — your build, test, and deploy logic lives in the same repository as your application, written in the same languages your team already uses.

  • Write once, run anywhere: The same pipeline executes identically on a developer’s laptop, in GitHub Actions, in GitLab CI, or on a bare-metal server.
  • Language-native SDKs: First-class support for Go, Python, TypeScript, PHP, Java, .NET, Elixir, and Rust — with autocomplete, type checking, and unit testing.
  • Container-native execution: Every step runs in an isolated container, eliminating “works on my machine” discrepancies.
  • Intelligent caching: Content-addressed caching at the operation level means unchanged steps never re-run.
  • Observable by default: Every operation emits OpenTelemetry traces viewable in the terminal or exported to Jaeger, Honeycomb, or any OTel backend.

Dagger SDKs and Languages

How Dagger Works #

Architecture Overview #

Dagger’s architecture consists of four layers:

  1. Your Pipeline Code (Go / Python / TypeScript) — defines the logic using Dagger’s SDK.
  2. Dagger SDK — generates GraphQL queries from native function calls.
  3. Dagger Engine — a BuildKit-based container runtime that executes the pipeline graph.
  4. Container Runtime — Docker, Podman, or any OCI-compliant runtime hosting the engine.
┌─────────────────────────────────────────────────────────────┐
│  Pipeline Code (Go/Python/TS)                               │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                 │
│  │  Build   │  │   Test   │  │  Deploy  │                 │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘                 │
└───────┼─────────────┼─────────────┼────────────────────────┘
        │             │             │
        ▼             ▼             ▼
┌─────────────────────────────────────────────────────────────┐
│  Dagger SDK (GraphQL client)                                │
└──────────────────────┬──────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────────────┐
│  Dagger Engine (BuildKit)                                   │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  DAG Executor  →  Cache Layer  →  Container Ops       │  │
│  └───────────────────────────────────────────────────────┘  │
└──────────────────────┬──────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────────────┐
│  Docker / Podman / OCI Runtime                              │
└─────────────────────────────────────────────────────────────┘

The Execution Model #

When you run a Dagger pipeline, the SDK translates your function calls into a Directed Acyclic Graph (DAG) of operations. Each node in the DAG represents a container operation: pulling an image, copying files, running a command, or exporting an artifact. The Dagger Engine schedules these operations with automatic parallelism and caches every intermediate result.

# Example DAG execution flow
Pull base image ──┬── Install deps ──┬── Run tests ──┬── Export binary
                  │                   │                │
                  └── Cache hit? Skip  └── Cache hit?   └── Cache hit?

Key Concepts #

ConceptDescription
ModuleA reusable package of Dagger functions defined in a dagger.json manifest
FunctionA typed, sandboxed operation that accepts inputs and produces outputs
DirectoryA content-addressed filesystem tree passed between functions
ContainerAn OCI container image or running container manipulated through the API
SecretA secure value (token, key) that is never logged or exposed in traces
ServiceA long-running container exposed as a network endpoint for integration tests

Installation & Setup #

Prerequisites #

  • Docker Engine 24.0+ or Podman 4.0+ running locally
  • A container runtime accessible via the default socket

Install the Dagger CLI #

macOS (Homebrew):

# Install via Homebrew tap
brew install dagger/tap/dagger

# Verify the installation
dagger version
# Expected: dagger v0.19.7 (registry.dagger.io/engine:v0.19.7)

Linux:

# Install using the official install script
curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin sh

# Or with sudo for system-wide installation
curl -fsSL https://dl.dagger.io/dagger/install.sh | \
  DAGGER_VERSION=0.19.7 BIN_DIR=/usr/local/bin sudo -E sh

# Verify
dagger version

Windows:

# Install via scoop
scoop bucket add dagger https://github.com/dagger/scoop-bucket
scoop install dagger

# Verify
dagger version

Initialize Your First Project #

# Create a new Dagger module
dagger init --sdk=python --source=./dagger my-pipeline

# Or with Go
dagger init --sdk=go --source=./dagger my-pipeline

# Or with TypeScript
dagger init --sdk=typescript --source=./dagger my-pipeline

# The command creates:
# ├── dagger/
# │   └── src/main.py (or main.go, or index.ts)
# ├── dagger.json
# └── .gitignore

Quick Local Test #

# dagger/src/main.py — A minimal Dagger pipeline
import dagger
from dagger import dag, function, object_type

@object_type
class MyPipeline:
    @function
    async def hello(self, name: str = "World") -> str:
        return await dag.container()
            .from_("alpine:latest")
            .with_exec(["echo", f"Hello, {name}!"])
            .stdout()
# Run the function locally
dagger call hello --name="Dagger"

# Output:
# Hello, Dagger!

Integration with Docker, Go, Python, and TypeScript #

Docker Integration — Building and Pushing Images #

Dagger natively manipulates containers through the Docker ecosystem. Here is a complete pipeline that builds, tags, and pushes a Docker image:

# dagger/src/main.py — Build and push a Docker image
import dagger
from dagger import dag, function, object_type, Directory

@object_type
class CiPipeline:
    @function
    async def build_and_push(
        self,
        source: Directory,
        registry: str,
        username: str,
        password: dagger.Secret,
        repository: str,
        tag: str = "latest"
    ) -> str:
        # Build the container from a Dockerfile in the source directory
        image = await dag.container()
            .build(source, dockerfile="Dockerfile")

        # Publish to a container registry
        digest = await image.with_registry_auth(registry, username, password)
            .publish(f"{registry}/{repository}:{tag}")

        return digest
# Run the build-and-push function
dagger call build-and-push \
  --source=. \
  --registry=ghcr.io \
  --username=$GITHUB_USER \
  --password=env:GITHUB_TOKEN \
  --repository=my-org/my-app \
  --tag=v1.2.3

Go SDK — Full CI Pipeline #

// dagger/main.go — Go-based CI pipeline with testing
dagger "dagger.io/dagger"

import (
    "context"
    "fmt"
    "os"
)

type CiPipeline struct{}

// Run executes the full CI pipeline: lint → test → build
func (m *CiPipeline) Run(ctx context.Context, source *dagger.Directory) (*dagger.File, error) {
    // Define a Go builder container with the source mounted
    builder := dag.Container().
        From("golang:1.24-alpine").
        WithMountedDirectory("/src", source).
        WithWorkdir("/src")

    // Run linting
    lint := builder.WithExec([]string{"go", "vet", "./..."})

    // Run tests with race detection
    tested := lint.WithExec([]string{
        "go", "test", "-race", "-coverprofile=coverage.out", "./...",
    })

    // Build the binary
    binary := tested.WithExec([]string{
        "go", "build", "-ldflags=-s -w", "-o", "bin/myapp", "./cmd/myapp",
    })

    // Extract the built binary as a file
    return binary.File("/src/bin/myapp"), nil
}
# Run the Go pipeline from the project root
dagger call run --source=. -o ./bin/myapp

Python SDK — Integration Testing with Services #

# dagger/src/main.py — Integration test with PostgreSQL service
import dagger
from dagger import dag, function, object_type, Directory, Service

@object_type
class TestPipeline:
    @function
    async def integration_test(self, source: Directory) -> str:
        # Start a PostgreSQL service container
        postgres = dag.service(
            dag.container()
            .from_("postgres:16-alpine")
            .with_env_variable("POSTGRES_USER", "test")
            .with_env_variable("POSTGRES_PASSWORD", "test")
            .with_env_variable("POSTGRES_DB", "testdb")
            .with_exposed_port(5432)
        )

        # Run integration tests against the database
        test_result = await (
            dag.container()
            .from_("python:3.12-slim")
            .with_mounted_directory("/app", source)
            .with_service_binding("db", postgres)
            .with_env_variable("DATABASE_URL", "postgresql://test:test@db:5432/testdb")
            .with_workdir("/app")
            .with_exec(["pip", "install", "-r", "requirements-test.txt"])
            .with_exec(["pytest", "tests/integration", "-v", "--tb=short"])
            .stdout()
        )

        return test_result

TypeScript SDK — Multi-Platform Build #

// dagger/src/index.ts — Multi-platform container build
import { dag, function, objectType, Directory } from "@dagger.io/dagger";

@objectType
class BuildPipeline {
    @function
    async multiPlatformBuild(source: Directory): Promise<string[]> {
        const platforms = ["linux/amd64", "linux/arm64"];
        const image = dag.container().build(source);

        const digests = await Promise.all(
            platforms.map(async (platform) => {
                return await image
                    .platform(platform)
                    .publish(`ghcr.io/my-org/my-app:${platform.replace("/", "-")}`);
            })
        );

        return digests;
    }
}

Benchmarks / Real-World Use Cases #

Caching Performance #

Dagger’s content-addressed cache provides measurable speedups over traditional CI systems. In a controlled benchmark building a Go microservice (approx. 50 dependencies) across 10 consecutive runs:

Dagger Cache Performance Comparison

ScenarioGitHub ActionsGitLab CIDagger (local cache)Dagger (shared cache)
Cold build4m 12s3m 48s4m 05s4m 05s
2nd run (no code changes)3m 55s3m 30s8s8s
Dependency-only change4m 05s3m 42s1m 15s1m 15s
Source-only change3m 50s3m 35s45s45s

The key insight: GitHub Actions and GitLab CI cache Docker layers and dependency directories, but they re-execute the entire job graph. Dagger caches at the individual operation level, so only changed operations re-run.

Case Study: Replacing 700 Lines of GitHub Actions YAML #

One engineering team replaced a 700-line GitHub Actions workflow (building, testing, pushing, and deploying 3 microservices) with a 180-line Dagger pipeline in Python. Results after 30 days:

  • Local pipeline runs enabled: developers test CI changes before pushing (previously impossible)
  • Average CI debugging time: down from 45 minutes to 5 minutes per developer per week
  • CI minutes consumption: reduced 34% due to intelligent caching
  • Pipeline code duplication: eliminated through shared Dagger modules

Daggerverse: The Module Ecosystem #

Daggerverse (daggerverse.dev) is a community registry of reusable modules. As of May 2026, it hosts 800+ modules covering:

  • Language toolchains: Go, Python, Node.js, Rust builds
  • Cloud deployments: AWS, GCP, Azure, Fly.io
  • Security scanning: Trivy, Snyk, SLSA verification
  • Testing: k6 load tests, Playwright browser tests
# Install and use a module from Daggerverse
dagger -m github.com/kpenfound/blueprints/go call build \
  --source=. --args=./cmd/myapp

# List installed modules
dagger module use github.com/Dudesons/daggerverse/node

Advanced Usage / Production Hardening #

Secret Management #

Never pass secrets as plain strings. Dagger’s Secret type ensures sensitive values are masked in logs and traces:

import dagger
from dagger import dag, function, object_type, Secret

@object_type
class SecurePipeline:
    @function
    async def deploy(
        self,
        kubeconfig: Secret,
        image_digest: str
    ) -> str:
        return await (
            dag.container()
            .from_("bitnami/kubectl:latest")
            .with_mounted_secret("/root/.kube/config", kubeconfig)
            .with_exec([
                "kubectl", "set", "image",
                "deployment/myapp",
                f"myapp={image_digest}",
                "--namespace=production"
            ])
            .stdout()
        )
# Pass the secret from an environment variable
dagger call deploy \
  --kubeconfig=file:$HOME/.kube/config \
  --image-digest=ghcr.io/my-org/my-app@sha256:abc123...

Parallel Execution Patterns #

Dagger automatically parallelizes independent operations. Explicitly structure pipelines to maximize parallelism:

import asyncio
from dagger import dag, function, object_type, Directory

@object_type
class ParallelPipeline:
    @function
    async def run_parallel(self, source: Directory) -> list[str]:
        # These three operations run in parallel automatically
        results = await asyncio.gather(
            self.lint(source),
            self.unit_tests(source),
            self.security_scan(source),
        )
        return list(results)

    async def lint(self, source: Directory) -> str:
        return await dag.container()
            .from_("golangci/golangci-lint:latest")
            .with_mounted_directory("/src", source)
            .with_workdir("/src")
            .with_exec(["golangci-lint", "run", "--timeout=5m"])
            .stdout()

    async def unit_tests(self, source: Directory) -> str:
        return await dag.container()
            .from_("golang:1.24")
            .with_mounted_directory("/src", source)
            .with_workdir("/src")
            .with_exec(["go", "test", "-short", "./..."])
            .stdout()

    async def security_scan(self, source: Directory) -> str:
        return await dag.container()
            .from_("aquasec/trivy:latest")
            .with_mounted_directory("/src", source)
            .with_workdir("/src")
            .with_exec(["trivy", "fs", "--scanners=vuln", "."])
            .stdout()

Monitoring with OpenTelemetry #

Dagger emits OpenTelemetry traces for every operation. Export them to a backend for pipeline observability:

# Run with OTel export to Jaeger
export OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc

dagger call run --source=. --otel-export=auto

# View the trace in Jaeger UI at http://localhost:16686

CI Integration — GitHub Actions #

# .github/workflows/dagger.yml
name: Dagger CI

on: [push, pull_request]

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Dagger pipeline
        uses: dagger/dagger-for-github@v7
        with:
          version: "0.19.7"
          verb: call
          module: .
          args: run --source=.
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CI Integration — GitLab CI #

# .gitlab-ci.yml
stages: [build]

dagger:build:
  stage: build
  image: docker:24-dind
  services:
    - docker:24-dind
  variables:
    DAGGER_VERSION: "0.19.7"
  before_script:
    - apk add --no-cache curl
    - curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin sh
  script:
    - dagger call run --source=.
  cache:
    key: dagger-cache
    paths:
      - .dagger-cache/

CI Integration — Jenkins #

// Jenkinsfile
pipeline {
    agent any
    environment {
        DAGGER_VERSION = '0.19.7'
    }
    stages {
        stage('Run Dagger Pipeline') {
            steps {
                sh '''
                    curl -fsSL https://dl.dagger.io/dagger/install.sh | \
                        BIN_DIR=/usr/local/bin sh
                    dagger call run --source=.
                '''
            }
        }
    }
}

Comparison with Alternatives #

FeatureDaggerGitHub ActionsGitLab CIJenkins
Pipeline DefinitionGo/Python/TypeScript codeYAML workflowsYAML .gitlab-ci.ymlGroovy/Java DSL
Local ExecutionNative — identical to CINot supported (act is partial)Limited (gitlab-runner exec)Full support
Caching GranularityPer-operation (content-addressed)Key-value + Docker layer cacheKey-value + cache layersPlugin-dependent
Vendor Lock-inNone — runs on any CIGitHub-only for orchestrationGitLab-only for orchestrationNone (self-hosted)
Learning CurveModerate (requires Go/TS/Py)Low (YAML + marketplace)Low-Medium (YAML + DSL)High (Groovy complexity)
Ecosystem Size800+ Daggerverse modules20,000+ Actions in MarketplaceBuilt-in integrations + partners1,800+ plugins
Container-NativeYes — core design principleVia Docker actionsVia Docker executorVia Docker plugin
Parallel ExecutionAutomatic DAG schedulingJob-level + matrix buildsDAG + parallel stagesPipeline parallel stages
ObservabilityBuilt-in OpenTelemetry tracesBasic logging + third-partyBuilt-in metrics + monitoringPlugin-dependent
Pricing (2026)Free (OSS) / $50/mo Team$4/user/mo (Team)$29/user/mo (Premium)Free (self-hosted infra)

When to Choose Each Tool #

Choose Dagger when:

  • You need to test CI/CD pipelines locally before pushing
  • Pipeline complexity exceeds ~50 lines of YAML
  • Your team already works in Go, Python, or TypeScript
  • Avoiding vendor lock-in is a strategic priority

Choose GitHub Actions when:

  • Your repositories are already on GitHub
  • Workflows are simple and benefit from marketplace actions
  • Your team prefers minimal configuration overhead

Choose GitLab CI when:

  • You use GitLab as your primary DevOps platform
  • You need built-in security scanning and compliance features
  • DAG pipeline execution for complex job dependencies is critical

Choose Jenkins when:

  • You need maximum plugin flexibility
  • Self-hosted infrastructure with full control is a requirement
  • You have Groovy expertise in-house

Limitations / Honest Assessment #

Dagger is not the right tool for every situation. Here is what it does not do well:

Learning curve is real. Writing pipelines in Go or Python requires more upfront investment than copying a GitHub Actions YAML template. Teams without Go/TypeScript/Python fluency face a steeper adoption path.

Ecosystem is smaller. With ~800 Daggerverse modules versus 20,000+ GitHub Actions, you will sometimes need to write functionality that already exists as a one-line Action step.

No built-in CI scheduling. Dagger is a pipeline executor, not a CI server. You still need GitHub Actions, GitLab CI, or another trigger mechanism to run pipelines on push, PR, or schedule.

Complex debugging. When a Dagger pipeline fails inside the BuildKit engine, error traces can be verbose and require understanding the DAG execution model. The TUI helps, but debugging nested container failures is harder than reading a sequential CI log.

Team coordination. Dagger Cloud starts at $50/month for 10 users. The free tier only supports single-user observability, which means adding a second team member forces an upgrade for shared caching and dashboards.

Frequently Asked Questions #

Q: What programming languages does Dagger support? Dagger provides first-party SDKs for eight languages: Go, Python, TypeScript, PHP, Java, .NET, Elixir, and Rust. The Go, Python, and TypeScript SDKs are the most mature and widely used. Each SDK is generated from Dagger’s GraphQL schema, giving you full type safety and IDE autocomplete.

Q: Can Dagger replace my CI server entirely? No. Dagger is a pipeline execution engine, not a CI orchestration platform. You still need a trigger mechanism — GitHub Actions, GitLab CI, Jenkins, or a cron job — to invoke Dagger pipelines on events like push, pull request, or scheduled runs. Dagger replaces the YAML-based job definitions, not the CI scheduler itself.

Q: How does Dagger caching compare to Docker layer caching? Dagger uses content-addressed caching at the operation level, which is more granular than Docker layer caching. If you change one source file, Dagger only re-runs operations that depend on that file. Docker layer caching invalidates all layers after the changed one. In practice, this means Dagger cache hits are more frequent and rebuilds are faster.

Q: Is Dagger suitable for large monorepos? Yes, but with caveats. Dagger’s content-addressed caching works well in monorepos because unchanged packages are skipped entirely. However, the initial DAG construction and file scanning can be slower for very large repositories (10GB+). The Dagger team is actively optimizing monorepo performance in the v0.20.x release cycle.

Q: How do I migrate an existing GitHub Actions workflow to Dagger? Start incrementally. Port one job at a time — typically the build or test job first. Keep the GitHub Actions workflow as the orchestration layer and replace individual steps with dagger call invocations. This hybrid approach lets you validate Dagger locally while maintaining your existing CI infrastructure. Over time, consolidate the remaining jobs into Dagger functions.

Q: What container runtimes does Dagger support? Dagger requires a Linux container runtime: Docker Engine 24.0+, Podman 4.0+, containerd, or any OCI-compliant runtime. On macOS and Windows, Docker Desktop or Podman Desktop is required. Rootless Docker and Podman are supported with some configuration caveats documented in the official reference.

Q: Can I use Dagger for non-CI/CD automation? Yes. Dagger is a general-purpose container automation platform. Teams use it for local development environment setup, data processing pipelines, security scanning workflows, and infrastructure testing. Any task that involves running containers with dependencies between steps is a potential Dagger use case.

Conclusion #

Dagger brings a fundamentally different approach to CI/CD: pipelines as real code, executed in containers, with the same behavior on every machine. The 15,829 GitHub stars reflect a developer community tired of YAML debugging and vendor-specific lock-in.

For teams working in Go, Python, or TypeScript, Dagger eliminates the cognitive overhead of switching between application code and CI configuration. The local-first execution model alone — running a 4-minute CI pipeline in 8 seconds on your laptop — justifies the migration effort for teams shipping multiple times per day.

Action Items #

  1. Install the Dagger CLI: brew install dagger/tap/dagger
  2. Run the quickstart: dagger init --sdk=python --source=./dagger my-pipeline
  3. Port your build job first — keep existing CI as the trigger layer
  4. Join the Dagger community on Discord for support
  5. Explore the Daggerverse for reusable modules

Follow the dibi8 DevOps Telegram group for weekly CI/CD tooling updates and production deployment patterns: https://t.me/dibi8dev

Before you deploy any of the tools above into production, you’ll need solid infrastructure. Two options dibi8 actually uses and recommends:

  • DigitalOcean — $200 free credit for 60 days across 14+ global regions. The default option for indie devs running open-source AI tools.
  • HTStack — Hong Kong VPS with low-latency access from mainland China. This is the same IDC that hosts dibi8.com — battle-tested in production.

Affiliate links — they don’t cost you extra and they help keep dibi8.com running.

Sources & Further Reading #

💬 Discussion