Skip to main content

Cookbook

Filesystem

List host directory contents

The following code listing obtains a reference to the host working directory and lists the directory's contents.

package main

import (
"context"
"fmt"
"log"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()

client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
log.Println(err)
return
}
defer client.Close()

entries, err := client.Host().Directory(".").Entries(ctx)
if err != nil {
log.Println(err)
return
}

fmt.Println(entries)
}

Learn more

Get host directory with filters

The following code listing obtains a reference to the host working directory containing all files except *.txt files.

package main

import (
"context"
"fmt"
"log"
"os"

"dagger.io/dagger"
)

func main() {
os.WriteFile("foo.txt", []byte("1"), 0600)
os.WriteFile("bar.txt", []byte("2"), 0600)
os.WriteFile("baz.rar", []byte("3"), 0600)

ctx := context.Background()

client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
log.Println(err)
return
}
defer client.Close()

entries, err := client.Host().Directory(".", dagger.HostDirectoryOpts{
Exclude: []string{"*.txt"},
}).Entries(ctx)
if err != nil {
log.Println(err)
return
}

fmt.Println(entries)
}

The following code listing obtains a reference to the host working directory containing only *.rar files.

package main

import (
"context"
"fmt"
"log"
"os"

"dagger.io/dagger"
)

func main() {
os.WriteFile("foo.txt", []byte("1"), 0600)
os.WriteFile("bar.txt", []byte("2"), 0600)
os.WriteFile("baz.rar", []byte("3"), 0600)

ctx := context.Background()

client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
log.Println(err)
return
}
defer client.Close()

entries, err := client.Host().Directory(".", dagger.HostDirectoryOpts{
Include: []string{"*.rar"},
}).Entries(ctx)
if err != nil {
log.Println(err)
return
}

fmt.Println(entries)
}

The following code listing obtains a reference to the host working directory containing all files except *.rar files.

package main

import (
"context"
"fmt"
"log"
"os"

"dagger.io/dagger"
)

func main() {
os.WriteFile("foo.txt", []byte("1"), 0600)
os.WriteFile("bar.txt", []byte("2"), 0600)
os.WriteFile("baz.rar", []byte("3"), 0600)

ctx := context.Background()

client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
log.Println(err)
return
}
defer client.Close()

entries, err := client.Host().Directory(".", dagger.HostDirectoryOpts{
Include: []string{"*.*"},
Exclude: []string{"*.rar"},
}).Entries(ctx)
if err != nil {
log.Println(err)
return
}

fmt.Println(entries)
}

Learn more

Transfer and read host directory in container

The following code listing writes a host directory to a container at the /host container path and then reads the contents of the directory.

package main

import (
"context"
"fmt"
"log"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()

client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
log.Println(err)
return
}
defer client.Close()

contents, err := client.Container().
From("alpine:latest").
WithDirectory("/host", client.Host().Directory(".")).
WithExec([]string{"ls", "/host"}).
Stdout(ctx)
if err != nil {
log.Println(err)
return
}

fmt.Println(contents)
}

Transfer and write to host directory from container

The following code listing writes a host directory to a container at the /host container path, adds a file to it, and then exports the modified directory back to the host:

note

Modifications made to a host directory written to a container filesystem path do not appear on the host. Data flows only one way between Dagger operations, because they are connected in a DAG. To write modifications back to the host directory, you must explicitly export the directory back to the host filesystem.

package main

import (
"context"
"fmt"
"log"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()

client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
log.Println(err)
return
}
defer client.Close()

contents, err := client.Container().
From("alpine:latest").
WithDirectory("/host", client.Host().Directory("/tmp/sandbox")).
WithExec([]string{"/bin/sh", "-c", `echo foo > /host/bar`}).
Directory("/host").
Export(ctx, "/tmp/sandbox")
if err != nil {
log.Println(err)
return
}

fmt.Println(contents)
}

Learn more

Add Git repository as directory to container

The following code listing adds a remote Git repository branch to a container as a directory at the /src container path and then executes a command in the container to list the directory contents.

package main

import (
"context"
"fmt"
"log"
"os"

"dagger.io/dagger"
)

func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// get repository at specified branch
project := client.
Git("https://github.com/dagger/dagger").
Branch("main").
Tree()

// return container with repository
// at /src path
contents, err := client.Container().
From("alpine:latest").
WithDirectory("/src", project).
WithWorkdir("/src").
WithExec([]string{"ls", "/src"}).
Stdout(ctx)
if err != nil {
log.Println(err)
return
}

fmt.Println(contents)
}

Add Git repository as directory to container with filters

The following code listing adds a remote Git repository branch as a directory at the /src container path, excluding *.md files.

package main

import (
"context"
"fmt"
"log"
"os"

"dagger.io/dagger"
)

func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// get repository at specified branch
project := client.
Git("https://github.com/dagger/dagger").
Branch("main").
Tree()

// return container with repository
// at /src path
// excluding *.md files
contents, err := client.Container().
From("alpine:latest").
WithDirectory("/src", project, dagger.ContainerWithDirectoryOpts{
Exclude: []string{"*.md"},
}).
WithWorkdir("/src").
WithExec([]string{"ls", "/src"}).
Stdout(ctx)
if err != nil {
log.Println(err)
return
}

fmt.Println(contents)
}

The following code listing adds a remote Git repository branch as a directory at the /src container path, including only *.md files.

package main

import (
"context"
"fmt"
"log"
"os"

"dagger.io/dagger"
)

func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// get repository at specified branch
project := client.
Git("https://github.com/dagger/dagger").
Branch("main").
Tree()

// return container with repository
// at /src path
// including only *.md files
contents, err := client.Container().
From("alpine:latest").
WithDirectory("/src", project, dagger.ContainerWithDirectoryOpts{
Include: []string{"*.md"},
}).
WithWorkdir("/src").
WithExec([]string{"ls", "/src"}).
Stdout(ctx)
if err != nil {
log.Println(err)
return
}

fmt.Println(contents)
}

The following code listing adds a remote Git repository branch as a directory at the /src container path, including all *.md files except README.md.

package main

import (
"context"
"fmt"
"log"
"os"

"dagger.io/dagger"
)

func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// get repository at specified branch
project := client.
Git("https://github.com/dagger/dagger").
Branch("main").
Tree()

// return container with repository
// at /src path
// include all *.md files except README.md
contents, err := client.Container().
From("alpine:latest").
WithDirectory("/src", project, dagger.ContainerWithDirectoryOpts{
Include: []string{"*.md"},
Exclude: []string{"README.md"},
}).
WithWorkdir("/src").
WithExec([]string{"ls", "/src"}).
Stdout(ctx)
if err != nil {
log.Println(err)
return
}

fmt.Println(contents)
}

Builds

Perform multi-stage build

The following code listing performs a multi-stage build.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
// create dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// get host directory
project := client.Host().Directory(".")

// build app
builder := client.Container().
From("golang:latest").
WithDirectory("/src", project).
WithWorkdir("/src").
WithEnvVariable("CGO_ENABLED", "0").
WithExec([]string{"go", "build", "-o", "myapp"})

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

addr, err := prodImage.Publish(ctx, "localhost:5000/multistage")
if err != nil {
panic(err)
}

fmt.Println(addr)
}

Learn more

Perform matrix build

The following code listing builds separate images for multiple OS and CPU architecture combinations.

// Create a multi-build pipeline for a Go application.
package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
println("Building with Dagger")

// define build matrix
geese := []string{"linux", "darwin"}
goarches := []string{"amd64", "arm64"}

ctx := context.Background()
// initialize dagger client
c, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}

// get reference to the local project
src := c.Host().Directory(".")

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

golang := c.Container().
// get golang image
From("golang:latest").
// mount source code into golang image
WithDirectory("/src", src).
WithWorkdir("/src")

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

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

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

// write build artifacts to host
ok, err := outputs.Export(ctx, ".")
if err != nil {
panic(err)
}

if !ok {
panic("did not export files")
}
}

Learn more

Build multi-arch image

The following code listing builds a single image for different CPU architectures using native emulation.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

// the 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
}

// the container registry for the multi-platform image
const imageRepo = "localhost/testrepo:latest"

func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// the git repository containing code for the binary to be built
gitRepo := client.Git("https://github.com/dagger/dagger.git").
Branch("086862926433e19e1f24cd709e6165c36bdb2633").
Tree()

platformVariants := make([]*dagger.Container, 0, len(platforms))
for _, platform := range platforms {
// pull the golang image for this platform
ctr := client.Container(dagger.ContainerOpts{Platform: platform})
ctr = ctr.From("golang:1.20-alpine")

// mount in source code
ctr = ctr.WithDirectory("/src", gitRepo)

// mount in an empty dir where the built binary will live
ctr = ctr.WithDirectory("/output", client.Directory())

// ensure the binary will be statically linked and thus executable
// in the final image
ctr = ctr.WithEnvVariable("CGO_ENABLED", "0")

// build the binary and put the result at the mounted output
// directory
ctr = ctr.WithWorkdir("/src")
ctr = ctr.WithExec([]string{
"go", "build",
"-o", "/output/dagger",
"/src/cmd/dagger",
})

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

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

// publishing the final image uses the same API as single-platform
// images, but now additionally specify the `PlatformVariants`
// option with the containers built before.
imageDigest, err := client.
Container().
Publish(ctx, imageRepo, dagger.ContainerPublishOpts{
PlatformVariants: platformVariants,
// Some registries may require explicit use of docker mediatypes
// rather than the default OCI mediatypes
// MediaTypes: dagger.Dockermediatypes,
})
if err != nil {
panic(err)
}
fmt.Println("Pushed multi-platform image w/ digest: ", imageDigest)
}

Learn more

Build multi-arch image with cross-compilation

The following code listing builds a single image for different CPU architectures using cross-compilation.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
platformFormat "github.com/containerd/containerd/platforms"
)

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
}

// the container registry for the multi-platform image
const imageRepo = "localhost/testrepo:latest"

// util that returns the architecture of the provided platform
func architectureOf(platform dagger.Platform) string {
return platformFormat.MustParse(string(platform)).Architecture
}

func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

gitRepo := client.Git("https://github.com/dagger/dagger.git").
Branch("086862926433e19e1f24cd709e6165c36bdb2633").
Tree()

platformVariants := make([]*dagger.Container, 0, len(platforms))
for _, platform := range platforms {
// pull the golang image for the *host platform*. This is
// accomplished by just not specifying a platform; the default
// is that of the host.
ctr := client.Container()
ctr = ctr.From("golang:1.20-alpine")

// mount in our source code
ctr = ctr.WithDirectory("/src", gitRepo)

// mount in an empty dir to put the built binary
ctr = ctr.WithDirectory("/output", client.Directory())

// ensure the binary will be statically linked and thus executable
// in the final image
ctr = ctr.WithEnvVariable("CGO_ENABLED", "0")

// configure the go compiler to use cross-compilation targeting the
// desired platform
ctr = ctr.WithEnvVariable("GOOS", "linux")
ctr = ctr.WithEnvVariable("GOARCH", architectureOf(platform))

// build the binary and put the result at the mounted output
// directory
ctr = ctr.WithWorkdir("/src")
ctr = ctr.WithExec([]string{
"go", "build",
"-o", "/output/dagger",
"/src/cmd/dagger",
})
// select the output directory
outputDir := ctr.Directory("/output")

// wrap the output directory in a new empty container marked
// with the platform
binaryCtr := client.
Container(dagger.ContainerOpts{Platform: platform}).
WithRootfs(outputDir)
platformVariants = append(platformVariants, binaryCtr)
}

// publishing the final image uses the same API as single-platform
// images, but now additionally specify the `PlatformVariants`
// option with the containers built before.
imageDigest, err := client.
Container().
Publish(ctx, imageRepo, dagger.ContainerPublishOpts{
PlatformVariants: platformVariants,
// Some registries may require explicit use of docker mediatypes
// rather than the default OCI mediatypes
// MediaTypes: dagger.Dockermediatypes,
})
if err != nil {
panic(err)
}
fmt.Println("published multi-platform image with digest", imageDigest)
}

Learn more

Build image from Dockerfile

The following code listing builds an image from a Dockerfile in the current working directory on the host.

package main

import (
"context"
"fmt"
"math"
"math/rand"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()

// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

contextDir := client.Host().Directory(".")

ref, err := contextDir.
DockerBuild().
Publish(ctx, fmt.Sprintf("ttl.sh/hello-dagger-%.0f", math.Floor(rand.Float64()*10000000))) //#nosec
if err != nil {
panic(err)
}

fmt.Printf("Published image to :%s\n", ref)
}

Learn more

Build image from Dockerfile using different build context

The following code listing builds an image from a Dockerfile using a build context directory in a different location than the current working directory.

package main

import (
"context"
"fmt"
"math"
"math/rand"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()

// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// get build context directory
contextDir := client.Host().Directory("/projects/myapp")

// get Dockerfile in different filesystem location
dockerfilePath := "/data/myapp/custom.Dockerfile"
dockerfile := client.Host().File(dockerfilePath)

// add Dockerfile to build context directory
workspace := contextDir.WithFile("custom.Dockerfile", dockerfile)

// build using Dockerfile
// publish the resulting container to a registry
ref, err := client.
Container().
Build(workspace, dagger.ContainerBuildOpts{
Dockerfile: "custom.Dockerfile",
}).
Publish(ctx, fmt.Sprintf("ttl.sh/hello-dagger-%.0f", math.Floor(rand.Float64()*10000000))) //#nosec
if err != nil {
panic(err)
}

fmt.Printf("Published image to :%s\n", ref)
}

Learn more

Add OCI annotations to image

The following code listing adds OpenContainer Initiative (OCI) annotations to an image.

package main

import (
"context"
"fmt"
"os"
"time"

"dagger.io/dagger"
)

func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// create and publish image with annotations
ctr := client.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")

addr, err := ctr.Publish(ctx, "ttl.sh/my-alpine")

// note: some registries (e.g. ghcr.io) may require explicit use
// of Docker mediatypes rather than the default OCI mediatypes
// addr, err := ctr.Publish(ctx, "ttl.sh/my-alpine", dagger.ContainerPublishOpts{
// MediaTypes: dagger.Dockermediatypes,
// })

if err != nil {
panic(err)
}

fmt.Println(addr)
}

Define build-time variables

The following code listing defines various environment variables for build purposes.

// Create a multi-build pipeline for a Go application.
package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
println("Building with Dagger")

// define build matrix
geese := []string{"linux", "darwin"}
goarches := []string{"amd64", "arm64"}

ctx := context.Background()
// initialize dagger client
c, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}

// get reference to the local project
src := c.Host().Directory(".")

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

golang := c.Container().
// get golang image
From("golang:latest").
// mount source code into golang image
WithDirectory("/src", src).
WithWorkdir("/src")

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

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

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

// write build artifacts to host
ok, err := outputs.Export(ctx, ".")
if err != nil {
panic(err)
}

if !ok {
panic("did not export files")
}
}

Learn more

Access private Git repository

The following code listing demonstrates how to access a private Git repository using SSH.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx)
if err != nil {
panic(err)
}
defer client.Close()

// Retrieve path of authentication agent socket from host
sshAgentPath := os.Getenv("SSH_AUTH_SOCK")

// Private repository with a README.md file at the root.
readme, err := client.
Git("git@private-repository.git", dagger.GitOpts{
SSHAuthSocket: client.Host().UnixSocket(sshAgentPath),
}).
Branch("main").
Tree().
File("README.md").
Contents(ctx)

if err != nil {
panic(err)
}

fmt.Println("readme", readme)
}

Invalidate cache

The following code listing demonstrates how to invalidate the Dagger pipeline operations cache and thereby force execution of subsequent pipeline steps, by introducing a volatile time variable at a specific point in the Dagger pipeline.

note

This is a temporary workaround until cache invalidation support is officially added to Dagger.

note

Changes in mounted cache volumes or secrets do not invalidate the Dagger pipeline operations cache.

package main

import (
"context"
"fmt"
"os"
"time"

"dagger.io/dagger"
)

func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// invalidate cache to force execution
// of second WithExec() operation
output, err := client.Pipeline("test").
Container().
From("alpine").
WithExec([]string{"apk", "add", "curl"}).
WithEnvVariable("CACHEBUSTER", time.Now().String()).
WithExec([]string{"apk", "add", "zip"}).
Stdout(ctx)
if err != nil {
panic(err)
}
fmt.Println(output)
}

Services

Expose service containers to host

The following code listing makes HTTP requests from the host to an HTTP service running in a Dagger pipeline.

package main

import (
"context"
"fmt"
"io"
"net/http"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()

// create Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))

if err != nil {
panic(err)
}
defer client.Close()

// create HTTP service container with exposed port 8080
httpSrv := client.Container().
From("python").
WithDirectory("/srv", client.Directory().WithNewFile("index.html", "Hello, world!")).
WithWorkdir("/srv").
WithExec([]string{"python", "-m", "http.server", "8080"}).
WithExposedPort(8080).
AsService()

// expose HTTP service to host
tunnel, err := client.Host().Tunnel(httpSrv).Start(ctx)
if err != nil {
panic(err)
}
defer tunnel.Stop(ctx)

// get HTTP service address
srvAddr, err := tunnel.Endpoint(ctx)
if err != nil {
panic(err)
}

// access HTTP service from host
res, err := http.Get("http://" + srvAddr)
if err != nil {
panic(err)
}
defer res.Body.Close()

// print response
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}

Learn more

Expose host services to containers

The following code listing shows how a database client in a Dagger pipeline can access a database service running on the host.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()

// create Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))

if err != nil {
panic(err)
}
defer client.Close()

// expose host service on port 3306
hostSrv := client.Host().Service([]dagger.PortForward{
{Frontend: 3306, Backend: 3306},
})

// create MariaDB container
// with host service binding
// execute SQL query on host service
out, err := client.Container().
From("mariadb:10.11.2").
WithServiceBinding("db", hostSrv).
WithExec([]string{"/bin/sh", "-c", "/usr/bin/mysql --user=root --password=secret --host=db -e 'SELECT * FROM mysql.user'"}).
Stdout(ctx)
if err != nil {
panic(err)
}
fmt.Println(out)
}

Learn more

Use transient database service for application tests

The following code listing creates a temporary MariaDB database service and binds it to an application container for unit/integration testing.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()

// create Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))

if err != nil {
panic(err)
}
defer client.Close()

// get MariaDB base image
mariadb := client.Container().
From("mariadb:10.11.2").
WithEnvVariable("MARIADB_USER", "user").
WithEnvVariable("MARIADB_PASSWORD", "password").
WithEnvVariable("MARIADB_DATABASE", "drupal").
WithEnvVariable("MARIADB_ROOT_PASSWORD", "root").
WithExposedPort(3306).
AsService()

// get Drupal base image
// install additional dependencies
drupal := client.Container().
From("drupal:10.0.7-php8.2-fpm").
WithExec([]string{"composer", "require", "drupal/core-dev", "--dev", "--update-with-all-dependencies"})

// add service binding for MariaDB
// run kernel tests using PHPUnit
test, err := drupal.
WithServiceBinding("db", mariadb).
WithEnvVariable("SIMPLETEST_DB", "mysql://user:password@db/drupal").
WithEnvVariable("SYMFONY_DEPRECATIONS_HELPER", "disabled").
WithWorkdir("/opt/drupal/web/core").
WithExec([]string{"../../vendor/bin/phpunit", "-v", "--group", "KernelTests"}).
Stdout(ctx)

if err != nil {
panic(err)
}

fmt.Println(test)
}

Learn more

Start and stop services

The following code listing demonstrates explicitly starting a Docker daemon for use in a test suite.

package main_test

import (
"context"
"testing"

"dagger.io/dagger"
"github.com/stretchr/testify/require"
)

func TestFoo(t *testing.T) {
ctx := context.Background()

c, err := dagger.Connect(ctx)
require.NoError(t, err)

dockerd, err := c.Container().From("docker:dind").AsService().Start(ctx)
require.NoError(t, err)

// dockerd is now running, and will stay running
// so you don't have to worry about it restarting after a 10 second gap

// then in all of your tests, continue to use an explicit binding:
_, err = c.Container().From("golang").
WithServiceBinding("docker", dockerd).
WithEnvVariable("DOCKER_HOST", "tcp://docker:2375").
WithExec([]string{"go", "test", "./..."}).
Sync(ctx)
require.NoError(t, err)

// or, if you prefer
// trust `Endpoint()` to construct the address
//
// note that this has the exact same non-cache-busting semantics as WithServiceBinding,
// since hostnames are stable and content-addressed
//
// this could be part of the global test suite setup.
dockerHost, err := dockerd.Endpoint(ctx, dagger.ServiceEndpointOpts{
Scheme: "tcp",
})
require.NoError(t, err)

_, err = c.Container().From("golang").
WithEnvVariable("DOCKER_HOST", dockerHost).
WithExec([]string{"go", "test", "./..."}).
Sync(ctx)
require.NoError(t, err)

// Service.Stop() is available to explicitly stop the service if needed
}

Learn more

Outputs

Publish image to registry

The following code listing publishes a container image to a remote registry (Docker Hub). Replace the DOCKER-HUB-USERNAME and DOCKER-HUB-PASSWORD placeholders with your Docker Hub username and password respectively.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
// initialize Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// set secret as string value
secret := client.SetSecret("password", "DOCKER-HUB-PASSWORD")

// create container
c := client.Container(dagger.ContainerOpts{Platform: "linux/amd64"}).
From("nginx:1.23-alpine").
WithNewFile("/usr/share/nginx/html/index.html", dagger.ContainerWithNewFileOpts{
Contents: "Hello from Dagger!",
Permissions: 0o400,
})

// use secret for registry authentication
addr, err := c.
WithRegistryAuth("docker.io", "DOCKER-HUB-USERNAME", secret).
Publish(ctx, "DOCKER-HUB-USERNAME/my-nginx")
if err != nil {
panic(err)
}

// print result
fmt.Println("Published at:", addr)
}

Learn more

Export image to host

The following code listing exports a container image from a Dagger pipeline to the host.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
// initialize Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// use NGINX container
// add new webserver index page
c := client.Container(dagger.ContainerOpts{Platform: "linux/amd64"}).
From("nginx:1.23-alpine").
WithNewFile("/usr/share/nginx/html/index.html", dagger.ContainerWithNewFileOpts{
Contents: "Hello from Dagger!",
Permissions: 0o400,
})

// export to host filesystem
val, err := c.Export(ctx, "/tmp/my-nginx.tar")
if err != nil {
panic(err)
}

// print result
fmt.Println("Exported image: ", val)
}

Learn more

Export container directory to host

The following code listing exports the contents of a container directory to the host's temporary directory.

package main

import (
"context"
"fmt"
"log"
"os"
"path/filepath"

"dagger.io/dagger"
)

func main() {
hostdir := os.TempDir()

ctx := context.Background()

client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
log.Println(err)
return
}
defer client.Close()

_, err = client.Container().From("alpine:latest").
WithWorkdir("/tmp").
WithExec([]string{"wget", "https://dagger.io"}).
Directory(".").
Export(ctx, hostdir)
if err != nil {
log.Println(err)
return
}
contents, err := os.ReadFile(filepath.Join(hostdir, "index.html"))
if err != nil {
log.Println(err)
return
}
fmt.Println(string(contents))
}

Learn more

Publish image to registry with multiple tags

The following code listing tags a container image multiple times and publishes it to a remote registry (Docker Hub). Set the Docker Hub username and password as host environment variables named DOCKERHUB_USERNAME and DOCKERHUB_PASSWORD respectively.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// load registry credentials from environment variables
username := os.Getenv("DOCKERHUB_USERNAME")
if username == "" {
panic("DOCKERHUB_USERNAME env var must be set")
}
passwordPlaintext := os.Getenv("DOCKERHUB_PASSWORD")
if passwordPlaintext == "" {
panic("DOCKERHUB_PASSWORD env var must be set")
}
password := client.SetSecret("password", passwordPlaintext)

// define multiple image tags
tags := [4]string{"latest", "1.0-alpine", "1.0", "1.0.0"}

// create and publish image with multiple tags
ctr := client.Container().
From("alpine").
WithRegistryAuth("docker.io", username, password)

for _, tag := range tags {
addr, err := ctr.Publish(ctx, fmt.Sprintf("%s/alpine:%s", username, tag))
if err != nil {
panic(err)
}
fmt.Println("Published: ", addr)
}
}

Secrets

Expose secret via environment variable

The following code listing demonstrates how to inject an environment variable in a container as a secret.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
// initialize Dagger client
ctx := context.Background()

if os.Getenv("GH_SECRET") == "" {
panic("Environment variable GH_SECRET is not set")
}

client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// read secret from host variable
secret := client.SetSecret("gh-secret", os.Getenv("GH_SECRET"))

// use secret in container environment
out, err := client.
Container().
From("alpine:3.17").
WithSecretVariable("GITHUB_API_TOKEN", secret).
WithExec([]string{"apk", "add", "curl"}).
WithExec([]string{"sh", "-c", `curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN"`}).
Stdout(ctx)
if err != nil {
panic(err)
}

fmt.Println(out)
}

Learn more

Expose secret via file

The following code listing demonstrates how to inject a file in a container as a secret.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
// initialize Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// read file
config, err := os.ReadFile("/home/USER/.config/gh/hosts.yml")
if err != nil {
panic(err)
}

// set secret to file contents
secret := client.SetSecret("ghConfig", string(config))

// mount secret as file in container
out, err := client.
Container().
From("alpine:3.17").
WithExec([]string{"apk", "add", "github-cli"}).
WithMountedSecret("/root/.config/gh/hosts.yml", secret).
WithWorkdir("/root").
WithExec([]string{"gh", "auth", "status"}).
Stdout(ctx)
if err != nil {
panic(err)
}

fmt.Println(out)
}

Learn more

Use secret in Dockerfile build

The following code listing demonstrates how to inject a secret into a Dockerfile build. The secret is automatically mounted in the build container at /run/secrets/SECRET-ID.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()

if os.Getenv("GH_SECRET") == "" {
panic("Environment variable GH_SECRET is not set")
}

// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// read secret from host variable
secret := client.SetSecret("gh-secret", os.Getenv("GH_SECRET"))

// set context directory for Dockerfile build
contextDir := client.Host().Directory(".")

// build using Dockerfile
// specify secrets for Dockerfile build
// secrets will be mounted at /run/secrets/[secret-name]
out, err := contextDir.
DockerBuild(dagger.DirectoryDockerBuildOpts{
Dockerfile: "Dockerfile",
Secrets: []*dagger.Secret{secret},
}).
Stdout(ctx)

if err != nil {
panic(err)
}

fmt.Println(out)
}

The sample Dockerfile below demonstrates the process of mounting the secret using a secret filesystem mount type and using it in the Dockerfile build process:

FROM alpine:3.17
RUN apk add curl
RUN --mount=type=secret,id=gh-secret curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $(cat /run/secrets/gh-secret)"

Learn more

Load secret from Google Cloud Secret Manager

The following code listing reads a secret (a GitHub API token) from Google Cloud Secret Manager and uses it in a Dagger pipeline to interact with the GitHub API.

Set up Application Default Credentials (ADC) and replace the PROJECT-ID and SECRET-ID placeholders with your Google Cloud project and secret identifiers respectively.

package main

import (
"context"
"fmt"
"os"

secretmanager "cloud.google.com/go/secretmanager/apiv1"
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"dagger.io/dagger"
)

func main() {
ctx := context.Background()

// get secret from Google Cloud Secret Manager
secretPlaintext, err := gcpGetSecretPlaintext(ctx, "PROJECT-ID", "SECRET-ID")
if err != nil {
panic(err)
}

// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// load secret into Dagger
secret := client.SetSecret("ghApiToken", string(secretPlaintext))

// use secret in container environment
out, err := client.
Container().
From("alpine:3.17").
WithSecretVariable("GITHUB_API_TOKEN", secret).
WithExec([]string{"apk", "add", "curl"}).
WithExec([]string{"sh", "-c", `curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN"`}).
Stdout(ctx)
if err != nil {
panic(err)
}

// print result
fmt.Println(out)
}

func gcpGetSecretPlaintext(ctx context.Context, projectID, secretID string) (string, error) {
secretUri := fmt.Sprintf("projects/%s/secrets/%s/versions/latest", projectID, secretID)

// initialize Google Cloud API client
gcpClient, err := secretmanager.NewClient(ctx)
if err != nil {
panic(err)
}
defer gcpClient.Close()

// retrieve secret
secReq := &secretmanagerpb.AccessSecretVersionRequest{
Name: secretUri,
}

res, err := gcpClient.AccessSecretVersion(ctx, secReq)
if err != nil {
panic(err)
}

secretPlaintext := res.Payload.Data

return string(secretPlaintext), nil
}

Learn more

Load secret from Hashicorp Vault

The following code listing reads a secret (a GitHub API token) from a Hashicorp Vault Key/Value v2 engine and uses it in a Dagger pipeline to interact with the GitHub API.

Set the Hashicorp Vault URI, namespace, role and secret identifiers as host environment variables named VAULT_ADDRESS, VAULT_NAMESPACE, VAULT_ROLE_ID and VAULT_SECRET_ID respectively. Replace the MOUNT-PATH, SECRET-ID and SECRET-KEY placeholders with your Hashicorp Vault mount point, secret identifier and key respectively.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
"github.com/hashicorp/vault-client-go"
"github.com/hashicorp/vault-client-go/schema"
)

func main() {
ctx := context.Background()

client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
if err != nil {
panic(err)
}
defer client.Close()

// get secret from Vault
secretPlaintext, err := getVaultSecret("MOUNT-PATH", "SECRET-ID", "SECRET-KEY")
if err != nil {
panic(err)
}

// load secret into Dagger
secret := client.SetSecret("ghApiToken", secretPlaintext)

// use secret in container environment
out, err := client.Container().
From("alpine:3.17").
WithSecretVariable("GITHUB_API_TOKEN", secret).
WithExec([]string{"apk", "add", "curl"}).
WithExec([]string{"sh", "-c", "curl \"https://api.github.com/repos/dagger/dagger/issues\" --header \"Accept: application/vnd.github+json\" --header \"Authorization: Bearer $GITHUB_API_TOKEN\""}).
Stdout(ctx)

// print result
fmt.Println(out)
}

func getVaultSecret(mountPath, secretID, secretKey string) (string, error) {
ctx := context.Background()

// check for required variables in host environment
address := os.Getenv("VAULT_ADDRESS")
role_id := os.Getenv("VAULT_ROLE_ID")
secret_id := os.Getenv("VAULT_SECRET_ID")

// create Vault client
client, err := vault.New(
vault.WithAddress(address),
)
if err != nil {
return "", err
}

// log in to Vault
resp, err := client.Auth.AppRoleLogin(
ctx,
schema.AppRoleLoginRequest{
RoleId: role_id,
SecretId: secret_id,
},
vault.WithMountPath(mountPath),
)
if err != nil {
return "", err
}

if err := client.SetToken(resp.Auth.ClientToken); err != nil {
return "", err
}

// read and return secret
secret, err := client.Secrets.KvV2Read(
ctx,
secretID,
vault.WithMountPath(mountPath),
)
if err != nil {
return "", err
}
return fmt.Sprintf("%s", secret.Data.Data[secretKey]), nil
}

Learn more

Mount directories as secrets in a container

The following code listing demonstrates how to securely mount directories as secrets in a container. The directory structure/file names will be accessible, but contents of the secrets will be scrubbed:

package main

import (
"context"
"fmt"
"os"
"path/filepath"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

gpgKey := os.Getenv("GPG_KEY")
if gpgKey == "" {
gpgKey = "public"
}

// Export file signature
ok, err := client.Container().
From("alpine:3.17").
WithExec([]string{"apk", "add", "--no-cache", "gnupg"}).
With(mountedSecretDirectory(client, "/root/.gnupg", "~/.gnupg")).
WithWorkdir("/root").
WithMountedFile("myapp", client.Host().File("myapp")).
WithExec([]string{"gpg", "--detach-sign", "--armor", "-u", gpgKey, "myapp"}).
File("myapp.asc").
Export(ctx, "myapp.asc")
if !ok || err != nil {
panic(err)
}

fmt.Println("Signature exported successfully")
}

func mountedSecretDirectory(client *dagger.Client, targetPath, sourcePath string) func(*dagger.Container) *dagger.Container {
return func(c *dagger.Container) *dagger.Container {
sourceDir := filepath.Join(os.Getenv("HOME"), sourcePath[2:])
filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

if info.Mode().IsRegular() {
relativePath, _ := filepath.Rel(sourceDir, path)
target := filepath.Join(targetPath, relativePath)
secret := client.Host().SetSecretFile(filepath.Base(path), path)
c = c.WithMountedSecret(target, secret)
}

return nil
})

// Fix directory permissions
c = c.WithExec([]string{"sh", "-c", fmt.Sprintf("find %s -type d -exec chmod 700 {} \\;", targetPath)})

return c
}
}

Error handling

Terminate gracefully

The following code listing demonstrates how to handle errors gracefully, without crashing the program or script running Dagger pipelines.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
if err := run(); err != nil {
// Don't panic
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run() error {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
return fmt.Errorf("dagger connect: %w", err)
}
defer client.Close()

err = Test(ctx, client)
if err != nil {
return fmt.Errorf("test pipeline: %w", err)
}

fmt.Println("Test passed!")
return nil
}

func Test(ctx context.Context, client *dagger.Client) error {
_, err := client.
Container().
From("alpine").
// ERROR: cat: read error: Is a directory
WithExec([]string{"cat", "/"}).
Sync(ctx)
return err
}

Handle exit code and unexpected errors

The following code listing demonstrates how to handle a non-zero exit code (an error from running a command) in a container, with several use cases:

  • Difference between “test failed” and “failed to test”
  • Handle a specific exit code value
  • Handle a failure from a command executed in a container, without checking for the exit code
  • Catching and handling a failure from a command executed in a container, without propagating it
  • Get the standard output of a command, irrespective of whether or not it failed
package main

import (
"context"
"errors"
"fmt"
"os"

"dagger.io/dagger"
)

// WarningExit is the exit code for warnings.
const WarningExit = 5

var reportCmd = `
echo "QA Checks"
echo "========="
echo "Check 1: PASS"
echo "Check 2: FAIL"
echo "Check 3: PASS"
exit 1
`

func main() {
if err := run(); err != nil {
// Don't panic
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run() error {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
return fmt.Errorf("dagger connect: %w", err)
}
defer client.Close()

err = Test(ctx, client)
if err != nil {
// Unexpected error (not from WithExec).
return fmt.Errorf("test pipeline: %w", err)
}

result, err := Report(ctx, client)
if err != nil {
// Unexpected error (not from WithExec).
return fmt.Errorf("report pipeline: %w", err)
}
fmt.Println(result)

return nil
}

func Test(ctx context.Context, client *dagger.Client) error {
_, err := client.
Container().
From("alpine").
WithExec([]string{"sh", "-c", "echo Skipped! >&2; exit 5"}).
Sync(ctx)

// Handle error from WithExec error here, but let other errors bubble up.
var e *dagger.ExecError
if errors.As(err, &e) {
// Don't do anything when skipped.
// Print message to stderr otherwise.
if e.ExitCode != WarningExit {
fmt.Fprintf(os.Stderr, "Test failed: %s", e.Stderr)
}
return nil
}
return err
}

func Report(ctx context.Context, client *dagger.Client) (string, error) {
output, err := client.
Container().
From("alpines"). // ⚠️ typo! non-exec failure
WithExec([]string{"sh", "-c", reportCmd}).
Stdout(ctx)

// Get stdout even on non-zero exit.
var e *dagger.ExecError
if errors.As(err, &e) {
// Not necessary to check for `e.ExitCode != 0`.
return e.Stdout, nil
}
return output, err
}

Continue using container after command execution fails

This code listing demonstrates how to continue using a container after a command executed within it fails. A common use case for this is to export a report that a test suite tool generates.

note

The caveat with this approach is that forcing a zero exit code on a failure caches the failure. This may not be desired depending on the use case.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

var script = `#!/bin/sh
echo "Test Suite"
echo "=========="
echo "Test 1: PASS" >> report.txt
echo "Test 2: FAIL" >> report.txt
echo "Test 3: PASS" >> report.txt
exit 1
`

func main() {
if err := run(); err != nil {
// Don't panic
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run() error {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
return err
}
defer client.Close()

return Test(ctx, client)
}

func Test(ctx context.Context, client *dagger.Client) error {
// The result of `Sync` is the container, which allows continued chaining.
ctr, err := client.
Container().
From("alpine").
// Add script with execution permission to simulate a testing tool.
WithNewFile("run-tests", dagger.ContainerWithNewFileOpts{
Contents: script,
Permissions: 0o750,
}).
// If the exit code isn't needed: "run-tests; true"
WithExec([]string{"sh", "-c", "/run-tests; echo -n $? > /exit_code"}).
Sync(ctx)
if err != nil {
// Unexpected error, could be network failure.
return fmt.Errorf("run tests: %w", err)
}

// Save report locally for inspection.
_, err = ctr.
File("report.txt").
Export(ctx, "report.txt")
if err != nil {
// Test suite ran but there's no report file.
return fmt.Errorf("get report: %w", err)
}

// Use the saved exit code to determine if the tests passed.
exitCode, err := ctr.File("/exit_code").Contents(ctx)
if err != nil {
return fmt.Errorf("get exit code: %w", err)
}

if exitCode != "0" {
fmt.Fprintln(os.Stderr, "Tests failed!")
} else {
fmt.Println("Tests passed!")
}

return nil
}

Optimizations

Cache dependencies

The following code listing uses a cache volume for application dependencies. This enables Dagger to reuse the contents of the cache every time the pipeline runs, and thereby speed up pipeline operations.

package main

import (
"context"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()

// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// use a golang:1.21 container
// mount the source code directory on the host
// at /src in the container
// mount the cache volume to persist dependencies
source := client.Container().
From("golang:1.21").
WithDirectory("/src", client.Host().Directory(".")).
WithWorkdir("/src").
WithMountedCache("/go/pkg/mod", client.CacheVolume("go-mod-121")).
WithEnvVariable("GOMODCACHE", "/go/pkg/mod").
WithMountedCache("/go/build-cache", client.CacheVolume("go-build-121")).
WithEnvVariable("GOCACHE", "/go/build-cache")

// set the working directory in the container
// install application dependencies
_, err = source.
WithExec([]string{"go", "build"}).
Sync(ctx)
if err != nil {
panic(err)
}
}

Learn more

Persist service state between runs

The following code listing uses a cache volume to persist a service's data across pipeline runs.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
ctx := context.Background()

// create Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))

if err != nil {
panic(err)
}
defer client.Close()

// create Redis service container
redisSrv := client.Container().
From("redis").
WithExposedPort(6379).
WithMountedCache("/data", client.CacheVolume("my-redis")).
WithWorkdir("/data").
AsService()

// create Redis client container
redisCLI := client.Container().
From("redis").
WithServiceBinding("redis-srv", redisSrv).
WithEntrypoint([]string{"redis-cli", "-h", "redis-srv"})

// set and save value
redisCLI.
WithExec([]string{"set", "foo", "abc"}).
WithExec([]string{"save"}).
Stdout(ctx)

// get value
val, err := redisCLI.
WithExec([]string{"get", "foo"}).
Stdout(ctx)

if err != nil {
panic(err)
}

fmt.Println(val)
}

Learn more

Add multiple environment variables to a container

The following code listing demonstrates how to add multiple environment variables to a container.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// setup container and
// define environment variables
ctr := client.
Container().
From("alpine").
With(EnvVariables(map[string]string{
"ENV_VAR_1": "VALUE 1",
"ENV_VAR_2": "VALUE 2",
"ENV_VAR_3": "VALUE 3",
})).
WithExec([]string{"env"})

// print environment variables
out, err := ctr.Stdout(ctx)
if err != nil {
panic(err)
}
fmt.Println(out)
}

func EnvVariables(envs map[string]string) dagger.WithContainerFunc {
return func(c *dagger.Container) *dagger.Container {
for key, value := range envs {
c = c.WithEnvVariable(key, value)
}
return c
}
}

Organize pipeline code into modules & classes

The following code listing demonstrates how to organize Dagger pipeline code into independent modules (or functions/packages, depending on your programming language) to improve code reusability and organization. It also demonstrates how to reuse the Dagger client and, therefore, share the Dagger session between modules.

note

The same Dagger client can safely be used in concurrent threads/routines. Therefore, it is recommended to reuse the Dagger client wherever possible, instead of creating a new client for each use. Initializing and using multiple Dagger clients in the same pipeline can result in unexpected behavior.

main.go
package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"

"main/alpine"
)

func main() {
ctx := context.Background()

// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// pass client to method imported from another module
fmt.Println(alpine.Version(client))
}
alpine/alpine.go
package alpine

import (
"context"

"dagger.io/dagger"
)

// create base image
func base(client *dagger.Client) *dagger.Container {
return client.
Container().
From("alpine:latest")
}

// run command in base image
func Version(client *dagger.Client) string {
ctx := context.Background()

out, err := base(client).
WithExec([]string{"cat", "/etc/alpine-release"}).
Stdout(ctx)
if err != nil {
panic(err)
}

return out
}

Another possible approach is to use independent classes (or interfaces, depending on the programming language) with public methods as functions. With this, it is no longer necessary to pass the client to all imported functions. The following code listing demonstrates this approach.

main.go
package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"

"main/alpine"
)

func main() {
ctx := context.Background()

// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// Create pipeline structure imported from another module passing the client
pipeline := alpine.New(client)

// Call version function
fmt.Println(pipeline.Version(ctx))
}
alpine/alpine.go
package alpine

import (
"context"
"dagger.io/dagger"
)

type Alpine struct {
client *dagger.Client
}

// Create a Alpine structure
func New(client *dagger.Client) *Alpine {
return &Alpine{client: client}
}

// create base image
func (a *Alpine) base() *dagger.Container {
return a.client.
Container().
From("alpine:latest")
}

// run command in base image
func (a *Alpine) Version(ctx context.Context) string {
out, err := a.
base().
WithExec([]string{"cat", "/etc/alpine-release"}).
Stdout(ctx)

if err != nil {
panic(err)
}

return out
}

Execute pipeline operations concurrently

The following code listing demonstrates how to use native-language concurrency features (goroutines in Go, promises in TypeScript, and task groups in Python) to execute pipeline operations in parallel.

package main

import (
"context"
"crypto/rand"
"log"
"math/big"
"os"

"golang.org/x/sync/errgroup"

"dagger.io/dagger"
)

func longTimeTask(ctx context.Context, c *dagger.Client) error {
sleepTime, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return err
}

_, err = c.Container().From("alpine").
WithExec([]string{"sleep", sleepTime.String()}).
WithExec([]string{"echo", "task done"}).
Sync(ctx)

return err
}

func main() {
ctx := context.Background()

client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
log.Println(err)
return
}
defer client.Close()

// Create err-group to handle error
eg, gctx := errgroup.WithContext(ctx)

// Launch task 1
eg.Go(func() error {
return longTimeTask(gctx, client)
})

// Launch task 2
eg.Go(func() error {
return longTimeTask(gctx, client)
})

// Launch task 3
eg.Go(func() error {
return longTimeTask(gctx, client)
})

// Wait for each task to be completed
err = eg.Wait()
if err != nil {
panic(err)
}
}

Integrations

Docker Engine

The following code listing shows how to connect to a Docker Engine on the host machine, by mounting the Docker UNIX socket into a container, and running the docker CLI.

package main

import (
"context"
"fmt"

"os"

"dagger.io/dagger"
)

func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// setup container with docker socket
ctr := client.
Container().
From("docker").
WithUnixSocket("/var/run/docker.sock", client.Host().UnixSocket("/var/run/docker.sock")).
WithExec([]string{"docker", "run", "--rm", "alpine", "uname", "-a"})

// print docker run
out, err := ctr.Stdout(ctx)
if err != nil {
panic(err)
}
fmt.Println(out)
}

Tailscale

The following code listing shows how to have a container running in a Dagger pipeline access a Tailscale network using Tailscale's userspace networking.

Set the TAILSCALE_AUTHKEY host environment variable to a Tailscale authentication key and the TAILSCALE_SERVICE_URL host environment variable to a URL accessibly only on the Tailscale network.

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

tailscaleAuthKey := os.Getenv("TAILSCALE_AUTHKEY")
if tailscaleAuthKey == "" {
panic("TAILSCALE_AUTHKEY env var must be set")
}
// set secret
authKeySecret := client.SetSecret("tailscaleAuthKey", tailscaleAuthKey)

tailscaleServiceURL := os.Getenv("TAILSCALE_SERVICE_URL")
if tailscaleServiceURL == "" {
panic("TAILSCALE_SERVICE_URL env var must be set")
}

// create Tailscale service container
tailscale := client.Container().
From("tailscale/tailscale:stable").
WithSecretVariable("TAILSCALE_AUTHKEY", authKeySecret).
WithExec([]string{"/bin/sh", "-c", "tailscaled --tun=userspace-networking --socks5-server=0.0.0.0:1055 --outbound-http-proxy-listen=0.0.0.0:1055 & tailscale up --authkey $TAILSCALE_AUTHKEY &"}).
WithExposedPort(1055)

// access Tailscale network
out, err := client.Container().
From("alpine:3.17").
WithExec([]string{"apk", "add", "curl"}).
WithServiceBinding("tailscale", tailscale).
WithEnvVariable("ALL_PROXY", "socks5://tailscale:1055/").
WithExec([]string{"curl", "--silent", "--verbose", tailscaleServiceURL}).
Sync(ctx)
if err != nil {
panic(err)
}

fmt.Println(out)
}

AWS Cloud Development Kit

The following code listing builds, publishes and deploys a container using the Amazon Web Services (AWS) Cloud Development Kit (CDK).

package main

import (
"context"
"fmt"
"os"

"dagger.io/dagger"
)

// build() reads the source code, run the tests and build the app and publish it to a container registry
func build(ctx context.Context, client *dagger.Client, registry *RegistryInfo) (string, error) {
nodeCache := client.CacheVolume("node")

// Read the source code from local directory
// sourceDir := client.Host().Directory("./app", dagger.HostDirectoryOpts{
// Exclude: []string{"node_modules/"},
// })

// Read the source code from a remote git repository
sourceDir := client.Git("https://github.com/dagger/hello-dagger.git").
Commit("5343dfee12cfc59013a51886388a7cacee3f16b9").
Tree()

source := client.Container().
From("node:16").
WithDirectory("/src", sourceDir).
WithMountedCache("/src/node_modules", nodeCache)

runner := source.
WithWorkdir("/src").
WithExec([]string{"npm", "install"})

test := runner.
WithExec([]string{"npm", "test", "--", "--watchAll=false"})

buildDir := test.
WithExec([]string{"npm", "run", "build"}).
Directory("./build")

// Explicitly build for "linux/amd64" to match the target (container on Fargate)
return client.Container(dagger.ContainerOpts{Platform: "linux/amd64"}).
From("nginx").
WithDirectory("/usr/share/nginx/html", buildDir).
WithRegistryAuth(
"125635003186.dkr.ecr.us-west-1.amazonaws.com",
registry.username,
client.SetSecret("registryPassword", registry.password),
).
Publish(ctx, registry.uri)
}

// deployToECS deploys a container image to the ECS cluster
func deployToECS(ctx context.Context, client *dagger.Client, awsClient *AWSClient, containerImage string) string {
stackParameters := map[string]string{
"ContainerImage": containerImage,
}

outputs, err := awsClient.cdkDeployStack(ctx, client, "DaggerDemoECSStack", stackParameters)
if err != nil {
panic(err)
}

return outputs["LoadBalancerDNS"]
}

func main() {
ctx := context.Background()

// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()

// initialize AWS client
awsClient, err := NewAWSClient(ctx, "us-west-1")
if err != nil {
panic(err)
}

// init the ECR Registry using the AWS CDK
registry := initRegistry(ctx, client, awsClient)
imageRef, err := build(ctx, client, registry)
if err != nil {
panic(err)
}
fmt.Println("Published image to", imageRef)

// init and deploy to ECS using the AWS CDK
publicDNS := deployToECS(ctx, client, awsClient, imageRef)

fmt.Printf("Deployed to http://%s/\n", publicDNS)
}
package main

import (
"context"
"encoding/base64"
"errors"
"fmt"
"log"
"os"
"strings"

"dagger.io/dagger"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/aws/jsii-runtime-go"
)

type AWSClient struct {
region string
cCfn *cloudformation.Client
cEcr *ecr.Client
}

func NewAWSClient(ctx context.Context, region string) (*AWSClient, error) {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return nil, err
}

cfg.Region = region
client := &AWSClient{
region: region,
cCfn: cloudformation.NewFromConfig(cfg),
cEcr: ecr.NewFromConfig(cfg),
}

return client, nil
}

func (c *AWSClient) GetCfnStackOutputs(ctx context.Context, stackName string) (map[string]string, error) {
out, err := c.cCfn.DescribeStacks(ctx, &cloudformation.DescribeStacksInput{
StackName: jsii.String(stackName),
})

if err != nil {
return nil, err
}

if len(out.Stacks) < 1 {
return nil, fmt.Errorf("cannot DescribeStack name %q", stackName)
}

stack := out.Stacks[0]
// status := string(stack.StackStatus)

return FormatStackOutputs(stack.Outputs), nil
}

func (c *AWSClient) GetECRAuthorizationToken(ctx context.Context) (string, error) {
log.Printf("ECR GetAuthorizationToken for region %q", c.region)
out, err := c.cEcr.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
if err != nil {
return "", err
}

if len(out.AuthorizationData) < 1 {
return "", fmt.Errorf("GetECRAuthorizationToken returned empty AuthorizationData")
}

authToken := *out.AuthorizationData[0].AuthorizationToken
return authToken, nil
}

// GetECRUsernamePassword fetches ECR auth token and converts it to username / password
func (c *AWSClient) GetECRUsernamePassword(ctx context.Context) (string, string, error) {
token, err := c.GetECRAuthorizationToken(ctx)
if err != nil {
return "", "", err
}

decoded, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return "", "", err
}

split := strings.SplitN(string(decoded), ":", 2)
if len(split) < 1 {
return "", "", fmt.Errorf("invalid base64 decoded data")
}

return split[0], split[1], nil
}

// FormatStackOutputs converts stack outputs into a map of string for easy printing
func FormatStackOutputs(outputs []types.Output) map[string]string {
outs := map[string]string{}

for _, o := range outputs {
outs[*o.OutputKey] = *o.OutputValue
}

return outs
}

// cdkDeployStack deploys a CloudFormation stack via the CDK cli
func (c *AWSClient) cdkDeployStack(ctx context.Context, client *dagger.Client, stackName string, stackParameters map[string]string) (map[string]string, error) {
cdkCode := client.Host().Directory("./infra", dagger.HostDirectoryOpts{
Exclude: []string{"cdk.out/", "infra"},
})

awsConfig := client.Host().Directory(os.ExpandEnv("${HOME}/.aws"))

cdkCommand := []string{"cdk", "deploy", "--require-approval=never", stackName}
// Append the stack parameters to the CLI, if there is any
for k, v := range stackParameters {
cdkCommand = append(cdkCommand, "--parameters", fmt.Sprintf("%s=%s", k, v))
}

_, err := client.Container().From("samalba/aws-cdk:2.65.0").
WithEnvVariable("AWS_REGION", c.region).
WithEnvVariable("AWS_DEFAULT_REGION", c.region).
WithDirectory("/opt/app", cdkCode).
WithDirectory("/root/.aws", awsConfig).
WithExec(cdkCommand).
Sync(ctx)

if err != nil {
var exErr *dagger.ExecError
if errors.As(err, &exErr) {
return nil, fmt.Errorf("cdk deploy exited with code %d", exErr.ExitCode)
}
return nil, err
}

outputs, err := c.GetCfnStackOutputs(ctx, stackName)
if err != nil {
return nil, err
}

return outputs, nil
}
package main

import (
"context"

"dagger.io/dagger"
)

type RegistryInfo struct {
uri string
username string
password string
}

// initRegistry creates and/or authenticate with an ECR repository
func initRegistry(ctx context.Context, client *dagger.Client, awsClient *AWSClient) *RegistryInfo {
outputs, err := awsClient.cdkDeployStack(ctx, client, "DaggerDemoECRStack", nil)
if err != nil {
panic(err)
}

repoURI := outputs["RepositoryUri"]

username, password, err := awsClient.GetECRUsernamePassword(ctx)
if err != nil {
panic(err)
}

return &RegistryInfo{repoURI, username, password}
}

Learn more

Google Cloud Run

The following code listing builds, publishes and deploys a container using Google Container Registry and Google Cloud Run.

package main

import (
"context"
"fmt"
"os"

run "cloud.google.com/go/run/apiv2"
runpb "cloud.google.com/go/run/apiv2/runpb"
"dagger.io/dagger"
)

const GCR_SERVICE_URL = "projects/PROJECT/locations/us-central1/services/myapp"
const GCR_PUBLISH_ADDRESS = "gcr.io/PROJECT/myapp"

func main() {
// create Dagger client
ctx := context.Background()
daggerClient, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer daggerClient.Close()

// get working directory on host
source := daggerClient.Host().Directory(".", dagger.HostDirectoryOpts{
Exclude: []string{"ci", "node_modules"},
})

// build application
node := daggerClient.Container(dagger.ContainerOpts{Platform: "linux/amd64"}).
From("node:16")

c := node.
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"cp", "-R", ".", "/home/node"}).
WithWorkdir("/home/node").
WithExec([]string{"npm", "install"}).
WithEntrypoint([]string{"npm", "start"})

// publish container to Google Container Registry
addr, err := c.Publish(ctx, GCR_PUBLISH_ADDRESS)
if err != nil {
panic(err)
}

// print ref
fmt.Println("Published at:", addr)

// create Google Cloud Run client
gcrClient, err := run.NewServicesClient(ctx)
if err != nil {
panic(err)
}
defer gcrClient.Close()

// define service request
gcrRequest := &runpb.UpdateServiceRequest{
Service: &runpb.Service{
Name: GCR_SERVICE_URL,
Template: &runpb.RevisionTemplate{
Containers: []*runpb.Container{
{
Image: addr,
Ports: []*runpb.ContainerPort{
{
Name: "http1",
ContainerPort: 1323,
},
},
},
},
},
},
}

// update service
gcrOperation, err := gcrClient.UpdateService(ctx, gcrRequest)
if err != nil {
panic(err)
}

// wait for service request completion
gcrResponse, err := gcrOperation.Wait(ctx)
if err != nil {
panic(err)
}

// print ref
fmt.Println("Deployment for image", addr, "now available at", gcrResponse.Uri)

}

Learn more

GitHub Actions

The following code listing shows how to integrate Dagger with GitHub Actions.

.github/workflows/dagger.yml
name: dagger
on:
push:
branches: [main]

jobs:
build:
name: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.20'
- name: Install Dagger CLI
run: cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sh; cd -; }
- name: Run Dagger pipeline
run: dagger run go run main.go

Learn more

GitLab CI

The following code listing shows how to integrate Dagger with GitLab CI.

.gitlab-ci.yml
.docker:
image: golang:1.20-alpine
services:
- docker:${DOCKER_VERSION}-dind
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_VERIFY: '1'
DOCKER_TLS_CERTDIR: '/certs'
DOCKER_CERT_PATH: '/certs/client'
DOCKER_DRIVER: overlay2
DOCKER_VERSION: '20.10.16'
.dagger:
extends: [.docker]
before_script:
- apk add docker-cli curl
- cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sh; cd -; }
build:
extends: [.dagger]
script:
- dagger run go run main.go

Learn more

CircleCI

The following code listing shows how to integrate Dagger with CircleCI.

.circleci/config.yml
version: 2.1
jobs:
build:
docker:
- image: cimg/go:1.20
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true
- run:
name: Install Dagger CLI
command: cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sudo sh; cd -; }
- run:
name: Run Dagger pipeline
command: dagger run --progress plain go run main.go
workflows:
dagger:
jobs:
- build

Learn more

Jenkins

The following code listing shows how to integrate Dagger with Jenkins.

Jenkinsfile
pipeline {
agent { label 'dagger' }

stages {
stage("dagger") {
steps {
sh '''
cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sh; cd -; }
dagger run go run main.go
'''
}
}
}
}

Requires docker client and go installed on your Jenkins agent, a Docker host available (can be docker:dind), and agents labeled in Jenkins with dagger.

Learn more

Azure Pipelines

The following code listing shows how to integrate Dagger with Azure Pipelines.

azure-pipelines.yml
trigger:
- master

pool:
name: 'Default'
vmImage: ubuntu-latest

steps:
- task: GoTool@0
inputs:
version: '1.20'

- script: cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sh; cd -; }
displayName: 'Install Dagger CLI'

- script: dagger run go run main.go
displayName: 'Run Dagger'

Learn more

AWS CodePipeline

The following code listing shows how to integrate Dagger with AWS CodePipeline.

buildspec.yml
version: 0.2

phases:
pre_build:
commands:
- echo "Installing Dagger CLI"
- cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sh; cd -; }

build:
commands:
- echo "Running Dagger pipeline"
- dagger run go run main.go

Learn more