Skip to main content

Builds

This page contains practical examples for building artifacts (files and directories) with Dagger. Each section below provides code examples in multiple languages and demonstrates different approaches to building artifacts.

Perform a multi-stage build

The following Dagger Function performs a multi-stage build.

package main

import (
"context"

"dagger/my-module/internal/dagger"
)

type MyModule struct{}

// Build and publish Docker container
func (m *MyModule) Build(
ctx context.Context,
// source code location
// can be local directory or remote Git repository
src *dagger.Directory,
) (string, error) {
// build app
builder := dag.Container().
From("golang:latest").
WithDirectory("/src", src).
WithWorkdir("/src").
WithEnvVariable("CGO_ENABLED", "0").
WithExec([]string{"go", "build", "-o", "myapp"})

// publish binary on alpine base
prodImage := dag.Container().
From("alpine").
WithFile("/bin/myapp", builder.File("/src/myapp")).
WithEntrypoint([]string{"/bin/myapp"})

// publish to ttl.sh registry
addr, err := prodImage.Publish(ctx, "ttl.sh/myapp:latest")
if err != nil {
return "", err
}

return addr, nil
}

Example

Perform a multi-stage build of the source code in the golang/example/hello repository and publish the resulting image:

dagger -c 'build https://github.com/golang/example#master:hello'

Perform a matrix build

The following Dagger Function performs a matrix build.

package main

import (
"context"
"dagger/my-module/internal/dagger"
"fmt"
)

type MyModule struct{}

// Build and return directory of go binaries
func (m *MyModule) Build(
ctx context.Context,
// Source code location
src *dagger.Directory,
) *dagger.Directory {
// define build matrix
gooses := []string{"linux", "darwin"}
goarches := []string{"amd64", "arm64"}

// create empty directory to put build artifacts
outputs := dag.Directory()

golang := dag.Container().
From("golang:latest").
WithDirectory("/src", src).
WithWorkdir("/src")

for _, goos := range gooses {
for _, goarch := range goarches {
// create directory for each OS and architecture
path := fmt.Sprintf("build/%s/%s/", goos, goarch)

// build artifact
build := golang.
WithEnvVariable("GOOS", goos).
WithEnvVariable("GOARCH", goarch).
WithExec([]string{"go", "build", "-o", path})

// add build to outputs
outputs = outputs.WithDirectory(path, build.Directory(path))
}
}

// return build directory
return outputs
}

Example

Perform a matrix build of the source code in the golang/example/hello repository and export build directory with go binaries for different operating systems and architectures.

dagger -c 'build https://github.com/golang/example#master:hello | export /tmp/matrix-builds'

Inspect the contents of the exported directory with tree /tmp/matrix-builds. The output should look like this:

/tmp/matrix-builds
└── build
├── darwin
│ ├── amd64
│ │ └── hello
│ └── arm64
│ └── hello
└── linux
├── amd64
│ └── hello
└── arm64
└── hello

8 directories, 4 files

Build multi-arch image

The following Dagger Function builds a single image for different CPU architectures using native emulation.

package main

import (
"context"

"dagger/my-module/internal/dagger"
)

type MyModule struct{}

// Build and publish multi-platform image
func (m *MyModule) Build(
ctx context.Context,
// Source code location
// can be local directory or remote Git repository
src *dagger.Directory,
) (string, error) {
// platforms to build for and push in a multi-platform image
var platforms = []dagger.Platform{
"linux/amd64", // a.k.a. x86_64
"linux/arm64", // a.k.a. aarch64
"linux/s390x", // a.k.a. IBM S/390
}

// container registry for the multi-platform image
const imageRepo = "ttl.sh/myapp:latest"

platformVariants := make([]*dagger.Container, 0, len(platforms))
for _, platform := range platforms {
// pull golang image for this platform
ctr := dag.Container(dagger.ContainerOpts{Platform: platform}).
From("golang:1.20-alpine").
// mount source code
WithDirectory("/src", src).
// mount empty dir where built binary will live
WithDirectory("/output", dag.Directory()).
// ensure binary will be statically linked and thus executable
// in the final image
WithEnvVariable("CGO_ENABLED", "0").
// build binary and put result at mounted output directory
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "/output/hello"})

// select output directory
outputDir := ctr.Directory("/output")

// wrap the output directory in the new empty container marked
// with the same platform
binaryCtr := dag.Container(dagger.ContainerOpts{Platform: platform}).
WithRootfs(outputDir)

platformVariants = append(platformVariants, binaryCtr)
}

// publish to registry
imageDigest, err := dag.Container().
Publish(ctx, imageRepo, dagger.ContainerPublishOpts{
PlatformVariants: platformVariants,
})

if err != nil {
return "", err
}

// return build directory
return imageDigest, nil
}

Example

Build and publish a multi-platform image:

dagger -c 'build https://github.com/golang/example#master:hello'

Build multi-arch image with cross-compliation

The following Dagger Function builds a single image for different CPU architectures using cross-compilation.

info

This Dagger Function uses the containerd utility module. To run it locally install the module first with dagger install github.com/levlaz/daggerverse/containerd@v0.1.2

package main

import (
"context"

"dagger/my-module/internal/dagger"
)

type MyModule struct{}

// Build and publish multi-platform image
func (m *MyModule) Build(
ctx context.Context,
// Source code location
// can be local directory or remote Git repository
src *dagger.Directory,
) (string, error) {
// platforms to build for and push in a multi-platform image
var platforms = []dagger.Platform{
"linux/amd64", // a.k.a. x86_64
"linux/arm64", // a.k.a. aarch64
"linux/s390x", // a.k.a. IBM S/390
}

// container registry for the multi-platform image
const imageRepo = "ttl.sh/myapp:latest"

platformVariants := make([]*dagger.Container, 0, len(platforms))
for _, platform := range platforms {
// parse architecture using containerd utility module
platformArch, err := dag.Containerd().ArchitectureOf(ctx, platform)

if err != nil {
return "", err
}

// pull golang image for the *host* platform, this is done by
// not specifying the a platform. The default is the host platform.
ctr := dag.Container().
From("golang:1.21-alpine").
// mount source code
WithDirectory("/src", src).
// mount empty dir where built binary will live
WithDirectory("/output", dag.Directory()).
// ensure binary will be statically linked and thus executable
// in the final image
WithEnvVariable("CGO_ENABLED", "0").
// configure go compiler to use cross-compilation targeting the
// desired platform
WithEnvVariable("GOOS", "linux").
WithEnvVariable("GOARCH", platformArch).
// build binary and put result at mounted output directory
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "/output/hello"})

// select output directory
outputDir := ctr.Directory("/output")

// wrap the output directory in the new empty container marked
// with the same platform
binaryCtr := dag.Container(dagger.ContainerOpts{Platform: platform}).
WithRootfs(outputDir).
WithEntrypoint([]string{"/hello"})

platformVariants = append(platformVariants, binaryCtr)
}

// publish to registry
imageDigest, err := dag.Container().
Publish(ctx, imageRepo, dagger.ContainerPublishOpts{
PlatformVariants: platformVariants,
})

if err != nil {
return "", err
}

// return build directory
return imageDigest, nil
}

Example

Build and publish a multi-platform image with cross compliation:

dagger -c 'build https://github.com/golang/example#master:hello'

Build image from Dockerfile

The following Dagger Function builds an image from a Dockerfile.

package main

import (
"context"

"dagger/my-module/internal/dagger"
)

type MyModule struct{}

// Build and publish image from existing Dockerfile
func (m *MyModule) Build(
ctx context.Context,
// location of directory containing Dockerfile
src *dagger.Directory,
) (string, error) {
ref, err := src.
DockerBuild(). // build from Dockerfile
Publish(ctx, "ttl.sh/hello-dagger")

if err != nil {
return "", err
}

return ref, nil
}

Example

Build and publish an image from an existing Dockerfile

dagger -c 'build https://github.com/dockersamples/python-flask-redis'

Build image from Dockerfile using different build context

The following function builds an image from a Dockerfile with a build context that is different than the current working directory.

package main

import (
"context"
"dagger/my-module/internal/dagger"
)

type MyModule struct{}

// Build and publish image from Dockerfile using a build context directory
// in a different location than the current working directory
func (m *MyModule) Build(
ctx context.Context,
// location of source directory
src *dagger.Directory,
// location of Dockerfile
dockerfile *dagger.File,
) (string, error) {

// get build context with dockerfile added
workspace := dag.Container().
WithDirectory("/src", src).
WithWorkdir("/src").
WithFile("/src/custom.Dockerfile", dockerfile).
Directory("/src")

// build using Dockerfile and publish to registry
ref, err := dag.Container().
Build(workspace, dagger.ContainerBuildOpts{
Dockerfile: "custom.Dockerfile",
}).
Publish(ctx, "ttl.sh/hello-dagger")

if err != nil {
return "", err
}

return ref, nil
}

Example

Build an image from the source code in https://github.com/dockersamples/python-flask-redis using the Dockerfile from a different build context, at https://github.com/vimagick/dockerfiles#master:registry-cli/Dockerfile:

dagger -c 'build https://github.com/dockersamples/python-flask-redis https://github.com/vimagick/dockerfiles#master:registry-cli/Dockerfile'

Cache application dependencies

The following Dagger Function uses a cache volume for application dependencies. This enables Dagger to reuse the contents of the cache across Dagger Function runs and reduce execution time.

package main

import "dagger/my-module/internal/dagger"

type MyModule struct{}

// Build an application using cached dependencies
func (m *MyModule) Build(
// Source code location
source *dagger.Directory,
) *dagger.Container {
return dag.Container().
From("golang:1.21").
WithDirectory("/src", source).
WithWorkdir("/src").
WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-121")).
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-121")).
WithEnvVariable("GOCACHE", "/go/build-cache").
WithExec([]string{"go", "build"})
}

Example

Build an application using cached dependencies:

dagger -c 'build .'

Execute functions concurrently

The following Dagger Function demonstrates how to use native-language concurrency features (errgroups in Go, task groups in Python), and promises in TypeScript to execute other Dagger Functions concurrently. If any of the concurrently-running functions fails, the remaining ones will be immediately cancelled.

package main

import (
"context"

"dagger/my-module/internal/dagger"

"golang.org/x/sync/errgroup"
)

// Constructor
func New(
source *dagger.Directory,
) *MyModule {
return &MyModule{
Source: source,
}
}

type MyModule struct {
Source *dagger.Directory
}

// Return the result of running unit tests
func (m *MyModule) Test(ctx context.Context) (string, error) {
return m.BuildEnv().
WithExec([]string{"npm", "run", "test:unit", "run"}).
Stdout(ctx)
}

// Return the result of running the linter
func (m *MyModule) Lint(ctx context.Context) (string, error) {
return m.BuildEnv().
WithExec([]string{"npm", "run", "lint"}).
Stdout(ctx)
}

// Return the result of running the type-checker
func (m *MyModule) Typecheck(ctx context.Context) (string, error) {
return m.BuildEnv().
WithExec([]string{"npm", "run", "type-check"}).
Stdout(ctx)
}

// Run linter, type-checker, unit tests concurrently
func (m *MyModule) RunAllTests(ctx context.Context) error {
// Create error group
eg, gctx := errgroup.WithContext(ctx)

// Run linter
eg.Go(func() error {
_, err := m.Lint(gctx)
return err
})

// Run type-checker
eg.Go(func() error {
_, err := m.Typecheck(gctx)
return err
})

// Run unit tests
eg.Go(func() error {
_, err := m.Test(gctx)
return err
})

// Wait for all tests to complete
// If any test fails, the error will be returned
return eg.Wait()
}

// Build a ready-to-use development environment
func (m *MyModule) BuildEnv() *dagger.Container {
nodeCache := dag.CacheVolume("node")
return dag.Container().
From("node:21-slim").
WithDirectory("/src", m.Source).
WithMountedCache("/root/.npm", nodeCache).
WithWorkdir("/src").
WithExec([]string{"npm", "install"})
}

Example

Execute a Dagger Function which performs different types of tests by executing other Dagger Functions concurrently.

dagger -c 'my-module $(host | directory .) | run-all-tests'

Persist service state across runs

The following Dagger Function uses a cache volume to persist a Redis service's data across Dagger Function runs.

package main

import (
"context"

"dagger/my-module/internal/dagger"
)

type MyModule struct{}

// Create Redis service and client
func (m *MyModule) Redis(ctx context.Context) *dagger.Container {
redisSrv := dag.Container().
From("redis").
WithExposedPort(6379).
WithMountedCache("/data", dag.CacheVolume("my-redis")).
WithWorkdir("/data").
AsService(dagger.ContainerAsServiceOpts{UseEntrypoint: true})

redisCLI := dag.Container().
From("redis").
WithServiceBinding("redis-srv", redisSrv).
WithEntrypoint([]string{"redis-cli", "-h", "redis-srv"})

return redisCLI
}

var execOpts = dagger.ContainerWithExecOpts{
UseEntrypoint: true,
}

// Set key and value in Redis service
func (m *MyModule) Set(
ctx context.Context,
// The cache key to set
key string,
// The cache value to set
value string,
) (string, error) {
return m.Redis(ctx).
WithExec([]string{"set", key, value}, execOpts).
WithExec([]string{"save"}, execOpts).
Stdout(ctx)
}

// Get value from Redis service
func (m *MyModule) Get(
ctx context.Context,
// The cache key to get
key string,
) (string, error) {
return m.Redis(ctx).
WithExec([]string{"get", key}, execOpts).
Stdout(ctx)
}

Example

  • Save data to a Redis service which uses a cache volume to persist a key named foo with value `123:

    dagger -c 'set foo 123'
  • Retrieve the value of the key foo after recreating the service state from the cache volume:

    dagger -c 'get foo'

Add OCI annotations to image

The following Dagger Function adds OpenContainer Initiative (OCI) annotations to an image.

package main

import (
"context"
"fmt"
"math"
"math/rand/v2"
)

type MyModule struct{}

// Build and publish image with OCI annotations
func (m *MyModule) Build(ctx context.Context) (string, error) {
address, err := dag.Container().
From("alpine:latest").
WithExec([]string{"apk", "add", "git"}).
WithWorkdir("/src").
WithExec([]string{"git", "clone", "https://github.com/dagger/dagger", "."}).
WithAnnotation("org.opencontainers.image.authors", "John Doe").
WithAnnotation("org.opencontainers.image.title", "Dagger source image viewer").
Publish(ctx, fmt.Sprintf("ttl.sh/custom-image-%.0f", math.Floor(rand.Float64()*10000000))) //#nosec
if err != nil {
return "", err
}
return address, nil
}

Example

Build and publish an image with OCI annotations:

dagger -c build

Add OCI labels to image

The following Dagger Function adds OpenContainer Initiative (OCI) labels to an image.

package main

import (
"context"
"time"
)

type MyModule struct{}

// Build and publish image with OCI labels
func (m *MyModule) Build(
ctx context.Context,
) (string, error) {
ref, err := dag.Container().
From("alpine").
WithLabel("org.opencontainers.image.title", "my-alpine").
WithLabel("org.opencontainers.image.version", "1.0").
WithLabel("org.opencontainers.image.created", time.Now().String()).
WithLabel("org.opencontainers.image.source", "https://github.com/alpinelinux/docker-alpine").
WithLabel("org.opencontainers.image.licenses", "MIT").
Publish(ctx, "ttl.sh/my-alpine")

if err != nil {
return "", err
}

return ref, nil
}

Example

Build and publish an image with OCI labels:

dagger -c build

Invalidate cache

The following function demonstrates how to invalidate the Dagger layer cache and force execution of subsequent workflow steps, by introducing a volatile time variable at a specific point in the Dagger workflow.

note
  • This is a temporary workaround until cache invalidation support is officially added to Dagger.
  • Changes in mounted cache volumes or secrets do not invalidate the Dagger layer cache.
package main

import (
"context"
"time"
)

type MyModule struct{}

// Run a build with cache invalidation
func (m *MyModule) Build(
ctx context.Context,
) (string, error) {
output, err := dag.Container().
From("alpine").
// comment out the line below to see the cached date output
WithEnvVariable("CACHEBUSTER", time.Now().String()).
WithExec([]string{"date"}).
Stdout(ctx)

if err != nil {
return "", err
}

return output, nil
}

Example

Print the date and time, invalidating the cache on each run. However, if the CACHEBUSTER environment variable is removed, the same value (the date and time on the first run) is printed on every run.

dagger -c build