- devops
- l2
- topic-pack
- dagger
- cicd --- Portal | Level: L2: Operations | Topics: Dagger / CI as Code, CI/CD | Domain: DevOps & Tooling
Dagger / CI as Code - Primer¶
Why This Matters¶
CI/CD pipelines are typically defined in YAML specific to one platform — GitHub Actions, GitLab CI, Jenkins. This creates vendor lock-in, makes pipelines hard to test locally, and forces you to express complex logic in a configuration language not designed for it. Dagger solves this by letting you write pipelines as real code (Go, Python, TypeScript) that runs identically on your laptop and in any CI system. Built by Solomon Hykes (the creator of Docker), Dagger uses containers as the execution primitive — every pipeline step runs in a container, cached by content-addressable digests. The result: pipelines that are testable, portable, and composable.
Core Concepts¶
1. Architecture¶
Dagger has three layers: - Dagger Engine — a daemon that runs containers and manages caching (uses BuildKit under the hood) - Dagger SDK — language-specific libraries (Go, Python, TypeScript) for defining pipelines - Dagger CLI — command-line tool for running and managing Dagger functions
# Install Dagger CLI
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
# Verify
dagger version
# The engine starts automatically on first use (runs as a container)
2. Pipeline Definition (Go Example)¶
// ci/main.go
package main
import (
"context"
"dagger/ci/internal/dagger"
)
type Ci struct{}
// Build and test a Go application
func (m *Ci) Build(ctx context.Context, source *dagger.Directory) *dagger.Container {
return dag.Container().
From("golang:1.22-alpine").
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "app", "."})
}
func (m *Ci) Test(ctx context.Context, source *dagger.Directory) (string, error) {
return dag.Container().
From("golang:1.22-alpine").
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "test", "-v", "./..."}).
Stdout(ctx)
}
// Lint with golangci-lint
func (m *Ci) Lint(ctx context.Context, source *dagger.Directory) (string, error) {
return dag.Container().
From("golangci/golangci-lint:latest").
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"golangci-lint", "run", "--timeout", "5m"}).
Stdout(ctx)
}
// Build and push a container image
func (m *Ci) Publish(
ctx context.Context,
source *dagger.Directory,
registry string,
username string,
password *dagger.Secret,
) (string, error) {
app := m.Build(ctx, source)
return app.
WithEntrypoint([]string{"/src/app"}).
WithRegistryAuth(registry, username, password).
Publish(ctx, registry+"/myapp:latest")
}
3. Pipeline Definition (Python Example)¶
# ci/src/main/__init__.py
import dagger
from dagger import dag, function, object_type
@object_type
class Ci:
@function
async def test(self, source: dagger.Directory) -> str:
"""Run pytest on the source code."""
return await (
dag.container()
.from_("python:3.11-slim")
.with_directory("/src", source)
.with_workdir("/src")
.with_exec(["pip", "install", "-r", "requirements.txt"])
.with_exec(["pytest", "-v", "--tb=short"])
.stdout()
)
@function
async def lint(self, source: dagger.Directory) -> str:
"""Run ruff linter."""
return await (
dag.container()
.from_("python:3.11-slim")
.with_directory("/src", source)
.with_workdir("/src")
.with_exec(["pip", "install", "ruff"])
.with_exec(["ruff", "check", "."])
.stdout()
)
@function
def build(self, source: dagger.Directory) -> dagger.Container:
"""Build a production container."""
return (
dag.container()
.from_("python:3.11-slim")
.with_directory("/app", source)
.with_workdir("/app")
.with_exec(["pip", "install", "--no-cache-dir", "-r", "requirements.txt"])
.with_entrypoint(["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0"])
)
4. Running Pipelines¶
# Initialize a Dagger module in your project
dagger init --sdk=go
dagger init --sdk=python
dagger init --sdk=typescript
# List available functions
dagger functions
# Run a function locally
dagger call test --source=.
dagger call build --source=.
dagger call lint --source=.
# Run with arguments
dagger call publish --source=. --registry=ghcr.io/myorg --username=bot \
--password=env:REGISTRY_TOKEN
# Interactive shell for debugging
dagger call build --source=. terminal
# View the execution plan
dagger call test --source=. --focus=false
5. Caching¶
Dagger caches aggressively using content-addressable storage. Same inputs produce the same cache key.
// Cache Go module downloads across runs
func (m *Ci) Build(ctx context.Context, source *dagger.Directory) *dagger.Container {
goCache := dag.CacheVolume("go-mod-cache")
goBuildCache := dag.CacheVolume("go-build-cache")
return dag.Container().
From("golang:1.22-alpine").
WithMountedCache("/go/pkg/mod", goCache).
WithMountedCache("/root/.cache/go-build", goBuildCache).
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "app", "."})
}
# Cache pip downloads in Python
@function
async def test(self, source: dagger.Directory) -> str:
pip_cache = dag.cache_volume("pip-cache")
return await (
dag.container()
.from_("python:3.11-slim")
.with_mounted_cache("/root/.cache/pip", pip_cache)
.with_directory("/src", source)
.with_workdir("/src")
.with_exec(["pip", "install", "-r", "requirements.txt"])
.with_exec(["pytest", "-v"])
.stdout()
)
6. CI Integration¶
Dagger runs identically in any CI. The CI system just needs Docker and the Dagger CLI.
GitHub Actions:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v6
with:
verb: call
args: test --source=.
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v6
with:
verb: call
args: lint --source=.
GitLab CI:
# .gitlab-ci.yml
test:
image: docker:latest
services:
- docker:dind
before_script:
- apk add curl && curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
script:
- dagger call test --source=.
7. Module Composition¶
Dagger modules can depend on other modules, creating reusable CI building blocks.
# Install a module dependency
dagger install github.com/purpleclay/daggerverse/golang@v0.5.0
# Use it in your module
dagger install github.com/dagger/dagger/modules/wolfi@main
// Use an installed module
func (m *Ci) Scan(ctx context.Context, source *dagger.Directory) (string, error) {
image := m.Build(ctx, source)
return dag.Trivy().
ScanContainer(image).
Output(ctx)
}
Quick Reference¶
# Setup
dagger init --sdk=go # or python, typescript
dagger develop # regenerate SDK code after schema changes
# Run
dagger functions # list available functions
dagger call <function> [args] # run a function
dagger call <function> terminal # debug with interactive shell
# Key advantages over YAML CI
# 1. Test pipelines locally: dagger call test --source=.
# 2. Same code runs in GitHub Actions, GitLab CI, Jenkins, CircleCI
# 3. Automatic caching with content-addressable storage
# 4. Real programming language: loops, conditionals, error handling
# 5. Type-safe: SDK catches errors at compile time
Wiki Navigation¶
Prerequisites¶
- CI/CD Pipelines & Patterns (Topic Pack, L1)
Related Content¶
- Adversarial Interview Gauntlet (30 sequences) (Scenario, L2) — CI/CD
- CI Pipeline Documentation (Reference, L1) — CI/CD
- CI/CD Drills (Drill, L1) — CI/CD
- CI/CD Flashcards (CLI) (flashcard_deck, L1) — CI/CD
- CI/CD Pipelines & Patterns (Topic Pack, L1) — CI/CD
- Circleci Flashcards (CLI) (flashcard_deck, L1) — CI/CD
- Deep Dive: CI/CD Pipeline Architecture (deep_dive, L2) — CI/CD
- GitHub Actions (Topic Pack, L1) — CI/CD
- Interview: CI Vuln Scan Failed (Scenario, L2) — CI/CD
- Jenkins Flashcards (CLI) (flashcard_deck, L1) — CI/CD