Build CI/CD pipelines as code using Dagger (the programmable container-native CI engine by Solomon Hykes). USE this skill when: user asks to create, debug, or optimize Dagger pipelines; write Dagger Functions or Modules in Go, Python, or TypeScript; use dagger CLI (dagger call, dagger init, dagger install); compose container operations (From, WithExec, WithDirectory, WithMountedCache); set up service bindings (AsService, WithServiceBinding); handle secrets in Dagger; integrate Dagger with GitHub Actions or GitLab CI; publish container images from Dagger; migrate from Dockerfiles or shell-based CI to Dagger; debug pipeline failures with dagger call --interactive. DO NOT use when: user wants plain Docker/docker-compose without Dagger; writing GitHub Actions YAML without Dagger; using other CI systems (Jenkins, CircleCI, Tekton) without Dagger involvement; general container questions unrelated to Dagger; Kubernetes operators or Helm charts not involving Dagger pipelines.
Dagger is a programmable CI/CD engine. Key components:
dagger binary. Entry point for invoking functions, initializing modules, installing dependencies.dagger.json. Composable via dagger install.dag.Container(), dag.Directory(), dag.SetSecret()).# Install Dagger CLI (macOS/Linux)
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
# Or via Homebrew
brew install dagger/tap/dagger
# Verify
dagger version
# Initialize a new module
dagger init --sdk=go # or --sdk=python or --sdk=typescript
# Creates: dagger.json, dagger/main.go (or .py/.ts), go.mod (or pyproject.toml/package.json)
# Install a dependency module
dagger install github.com/purpleclay/daggerverse/[email protected]
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Build compiles a Go application and returns the binary as a File.
func (m *MyModule) Build(ctx context.Context, src *dagger.Directory) *dagger.File {
return dag.Container().
From("golang:1.23-alpine").
WithMountedDirectory("/src", src).
WithMountedCache("/go/pkg/mod", dag.CacheVolume("gomod")).
WithMountedCache("/root/.cache/go-build", dag.CacheVolume("gobuild")).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "/app", "."}).
File("/app")
}
// Test runs unit tests and returns stdout.
func (m *MyModule) Test(ctx context.Context, src *dagger.Directory) (string, error) {
return dag.Container().
From("golang:1.23-alpine").
WithMountedDirectory("/src", src).
WithMountedCache("/go/pkg/mod", dag.CacheVolume("gomod")).
WithWorkdir("/src").
WithExec([]string{"go", "test", "./...", "-v"}).
Stdout(ctx)
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def build(self, source: dagger.Directory) -> dagger.Container:
return (
dag.container()
.from_("python:3.12-slim")
.with_directory("/app", source)
.with_workdir("/app")
.with_mounted_cache("/root/.cache/pip", dag.cache_volume("pip"))
.with_exec(["pip", "install", "-r", "requirements.txt"])
.with_exec(["python", "-m", "build"])
)
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
build(source: Directory): Container {
return dag
.container()
.from("node:20-slim")
.withDirectory("/app", source)
.withWorkdir("/app")
.withMountedCache("/app/node_modules", dag.cacheVolume("node-modules"))
.withExec(["npm", "ci"])
.withExec(["npm", "run", "build"])
}
}
Chain these methods on dag.Container():
| Method | Purpose |
|---|---|
.From(image) | Set base image |
.WithExec(args) | Run command in container |
.WithDirectory(path, dir) | Copy Directory into container |
.WithFile(path, file) | Copy single File into container |
.WithNewFile(path, contents) | Create file with inline contents |
.WithWorkdir(path) | Set working directory |
.WithEnvVariable(k, v) | Set environment variable |
.WithMountedCache(path, cache) | Mount persistent cache volume |
.WithMountedDirectory(path, dir) | Mount directory (not copy) |
.WithMountedSecret(path, secret) | Mount secret as file |
.WithSecretVariable(name, secret) | Inject secret as env var |
.WithServiceBinding(alias, svc) | Bind a service container |
.WithExposedPort(port) | Expose network port |
.WithUser(user) | Set user |
.WithEntrypoint(args) | Set entrypoint |
.File(path) | Extract a File from container |
.Directory(path) | Extract a Directory from container |
.Stdout(ctx) | Get stdout of last exec |
.Stderr(ctx) | Get stderr of last exec |
.Publish(address) | Push to OCI registry |
.AsService() | Convert to Service for binding |
.Terminal() | Open interactive debug shell |
Cache volumes persist across pipeline runs. Use aggressively. Layer ordering for cache efficiency: copy dependency manifests first, install deps, then copy source:
ctr := dag.Container().
From("golang:1.23").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("gomod")).
WithMountedCache("/root/.cache/go-build", dag.CacheVolume("gobuild")).
WithMountedDirectory("/src", src).
WithWorkdir("/src").
WithExec([]string{"go", "build", "./..."})
ctr = (
dag.container().from_("python:3.12-slim").with_workdir("/app")
# Copy only requirements first — this layer caches if deps unchanged
.with_file("/app/requirements.txt", source.file("requirements.txt"))
.with_mounted_cache("/root/.cache/pip", dag.cache_volume("pip"))
.with_exec(["pip", "install", "-r", "requirements.txt"])
# Then copy full source — only this layer busts on code change
.with_directory("/app", source)
)
Common cache volume names: gomod, gobuild, pip, npm, maven, cargo, gradle.
Never hardcode secrets. Use dag.SetSecret() or accept *dagger.Secret as function args:
func (m *MyModule) Deploy(ctx context.Context, src *dagger.Directory, token *dagger.Secret) (string, error) {
return dag.Container().
From("alpine:3.19").
WithSecretVariable("DEPLOY_TOKEN", token).
WithDirectory("/app", src).
WithExec([]string{"sh", "-c", "deploy.sh"}).
Stdout(ctx)
}
# Pass secrets via CLI
dagger call deploy --src=. --token=env:DEPLOY_TOKEN
dagger call deploy --src=. --token=file:./secret.txt
dagger call deploy --src=. --token=cmd:"vault read -field=token secret/deploy"
Registry authentication with secrets:
func (m *MyModule) Publish(ctx context.Context, ctr *dagger.Container, password *dagger.Secret) (string, error) {
return ctr.
WithRegistryAuth("ghcr.io", "username", password).
Publish(ctx, "ghcr.io/org/app:latest")
}
Spin up ephemeral services (databases, caches, APIs) for integration tests:
func (m *MyModule) IntegrationTest(ctx context.Context, src *dagger.Directory) (string, error) {
postgres := dag.Container().
From("postgres:16-alpine").
WithEnvVariable("POSTGRES_PASSWORD", "test").
WithEnvVariable("POSTGRES_DB", "testdb").
WithExposedPort(5432).
AsService()
return dag.Container().
From("golang:1.23").
WithServiceBinding("db", postgres).
WithEnvVariable("DATABASE_URL", "postgres://postgres:test@db:5432/testdb").
WithMountedDirectory("/src", src).
WithWorkdir("/src").
WithExec([]string{"go", "test", "./integration/...", "-v"}).
Stdout(ctx)
}
Service lifecycle: Dagger auto-starts services when bound, health-checks exposed ports, deduplicates identical services, tears down on pipeline completion.
# Install a remote module as a dependency
dagger install github.com/purpleclay/daggerverse/[email protected]
# Use installed module in your code
# Go: automatically available via dag.Golang()
# Python: dag.golang()
# TypeScript: dag.golang()
# List functions in any module
dagger functions -m github.com/shykes/daggerverse/hello
# Call a remote module function directly
dagger call -m github.com/shykes/daggerverse/hello hello --greeting="Hi" --name="World"
Publish your module: push your git repo containing dagger.json to GitHub. Others install via dagger install github.com/you/repo@version.