Cookbook
Filesystem
Copy a directory or remote repository to a container
The following Dagger Function accepts a Directory
argument, which could reference either a directory from the local filesystem or from a remote Git repository. It copies the specified directory to the /src
path in a container and returns the modified container.
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Return a container with a specified directory
func (m *MyModule) CopyDirectory(
ctx context.Context,
// Source directory
source *dagger.Directory,
) *dagger.Container {
return dag.Container().
From("alpine:latest").
WithDirectory("/src", source)
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
def copy_directory(
self, source: Annotated[dagger.Directory, Doc("Source directory")]
) -> dagger.Container:
"""Return a container with a specified directory"""
return dag.container().from_("alpine:latest").with_directory("/src", source)
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Return a container with a specified directory
*/
@func()
copyDirectory(
/**
* Source directory
*/
source: Directory,
): Container {
return dag.container().from("alpine:latest").withDirectory("/src", source)
}
}
Examples
-
Copy the
/myapp
host directory to/src
in the container and return the modified container:dagger call copy-directory --source=./myapp/
-
Copy the
dagger/dagger
GitHub repository to/src
in the container and return the modified container:dagger call copy-directory --source=github.com/dagger/dagger#main
-
Copy the
dagger/dagger
GitHub repository to/src
in the container and list the contents of the directory:dagger call \
copy-directory --source=https://github.com/dagger/dagger#main \
directory --path=/src \
entries
Modify a copied directory or remote repository in a container
The following Dagger Function accepts a Directory
argument, which could reference either a directory from the local filesystem or from a remote Git repository. It copies the specified directory to the /src
path in a container, adds a file to it, and returns the modified container.
Modifications made to a directory's contents after it is written to a container filesystem do not appear on the source. Data flows only one way between Dagger operations, because they are connected in a DAG. To transfer modifications back to the local host, you must explicitly export the directory back to the host filesystem.
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Return a container with a specified directory and an additional file
func (m *MyModule) CopyAndModifyDirectory(
ctx context.Context,
// Source directory
source *dagger.Directory,
) *dagger.Container {
return dag.Container().
From("alpine:latest").
WithDirectory("/src", source).
WithExec([]string{"/bin/sh", "-c", `echo foo > /src/foo`})
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
def copy_and_modify_directory(
self, source: Annotated[dagger.Directory, Doc("Source directory")]
) -> dagger.Container:
"""Return a container with a specified directory and an additional file"""
return (
dag.container()
.from_("alpine:latest")
.with_directory("/src", source)
.with_exec(["/bin/sh", "-c", "`echo foo > /src/foo`"])
)
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Return a container with a specified directory and an additional file
*/
@func()
copyAndModifyDirectory(
/**
* Source directory
*/
source: Directory,
): Container {
return dag
.container()
.from("alpine:latest")
.withDirectory("/src", source)
.withExec(["/bin/sh", "-c", "`echo foo > /src/foo`"])
}
}
Examples
-
Copy the
/myapp
host directory to/src
in the container, add a file to it, and return the modified container:dagger call copy-and-modify-directory --source=./myapp/
-
Copy the
dagger/dagger
GitHub repository to/src
in the container, add a file to it, and return the modified container:dagger call copy-and-modify-directory --source=github.com/dagger/dagger#main
-
Copy the
dagger/dagger
GitHub repository to/src
in the container, add a file to it, and list the contents of the directory:dagger call \
copy-and-modify-directory --source=https://github.com/dagger/dagger#main \
directory --path=/src \
entries
Copy a file to a container
The following Dagger Function accepts a File
argument representing the host file to be copied. It writes the specified file to a container in the /src/
directory and returns the modified container.
- Go
- Python
- TypeScript
package main
import (
"context"
"fmt"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Return a container with a specified file
func (m *MyModule) CopyFile(
ctx context.Context,
// Source file
f *dagger.File,
) *dagger.Container {
name, _ := f.Name(ctx)
return dag.Container().
From("alpine:latest").
WithFile(fmt.Sprintf("/src/%s", name), f)
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
async def copy_file(
self,
f: Annotated[dagger.File, Doc("Source file")],
) -> dagger.Container:
"""Return a container with a specified file"""
name = await f.name()
return dag.container().from_("alpine:latest").with_file(f"/src/{name}", f)
import { dag, Container, File, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Return a container with a specified file
*/
@func()
async copyFile(
/**
* Source file
*/
f: File,
): Promise<Container> {
const name = await f.name()
return dag.container().from("alpine:latest").withFile(`/src/${name}`, f)
}
}
Example
-
Copy the
/home/admin/archives.zip
file on the host to the/src
directory in the container and return the modified container:dagger call copy-file --f=/home/admin/archives.zip
-
Copy the
/home/admin/archives.zip
file on the host to the/src
directory in the container and list the contents of the directory:dagger call \
copy-file --f=/home/admin/archives.zip \
directory --path=/src \
entries
Copy a subset of a directory or remote repository to a container
The following Dagger Function accepts a Directory
argument, which could reference either a directory from the local filesystem or from a remote Git repository. It copies the specified directory to the /src
path in a container, using wildcard patterns to exclude specified sub-directories and files, and returns the modified container.
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Return a container with a filtered directory
func (m *MyModule) CopyDirectoryWithExclusions(
ctx context.Context,
// Source directory
source *dagger.Directory,
// Directory exclusion pattern
// +optional
excludeDirectoryPattern string,
// +optional
// File exclusion pattern
excludeFilePattern string,
) *dagger.Container {
filteredSource := source.
WithoutDirectory(excludeDirectoryPattern).
WithoutFile(excludeFilePattern)
return dag.Container().
From("alpine:latest").
WithDirectory("/src", filteredSource)
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
def copy_directory_with_exclusions(
self,
source: Annotated[dagger.Directory, Doc("Source directory")],
exclude_directory: Annotated[str, Doc("Directory exclusion pattern")] | None,
exclude_file: Annotated[str, Doc("File exclusion pattern")] | None,
) -> dagger.Container:
"""Return a container with a filtered directory"""
filtered_source = source
if exclude_directory is not None:
filtered_source = filtered_source.without_directory(exclude_directory)
if exclude_file is not None:
filtered_source = filtered_source.without_file(exclude_file)
return (
dag.container()
.from_("alpine:latest")
.with_directory("/src", filtered_source)
)
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Return a container with a filtered directory
*/
@func()
copyDirectoryWithExclusions(
/**
* Source directory
*/
source: Directory,
/**
* Directory exclusion pattern
*/
excludeDirectory?: string,
/**
* File exclusion pattern
*/
excludeFile?: string,
): Container {
let filteredSource = source
if (!excludeDirectory) {
filteredSource = filteredSource.withoutDirectory(excludeDirectory)
}
if (!excludeFile) {
filteredSource = filteredSource.withoutFile(excludeFile)
}
return dag
.container()
.from("alpine:latest")
.withDirectory("/src", filteredSource)
}
}
Examples
-
Copy the current host directory to
/src
in the container, excluding thedagger
sub-directory and thedagger.json
file, and return the modified container:dagger call copy-directory-with-exclusions --source=. --exclude-directory=dagger --exclude-file=dagger.json
-
Copy the
dagger/dagger
GitHub repository to/src
in the container, excluding all*.md
files, and list the contents of the directory:dagger call \
copy-directory-with-exclusions --source=https://github.com/dagger/dagger#main --exclude-file=*.md \
directory --path=/src \
entries
Builds
Perform a multi-stage build
The following Dagger Function performs a multi-stage build.
- Go
- Python
- TypeScript
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
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def build(self, src: dagger.Directory) -> str:
"""Build and publish Docker container"""
# build app
builder = (
dag.container()
.from_("golang:latest")
.with_directory("/src", src)
.with_workdir("/src")
.with_env_variable("CGO_ENABLED", "0")
.with_exec(["go", "build", "-o", "myapp"])
)
# publish binary on alpine base
prod_image = (
dag.container()
.from_("alpine")
.with_file("/bin/myapp", builder.file("/src/myapp"))
.with_entrypoint(["/bin/myapp"])
)
# publish to ttl.sh registry
addr = prod_image.publish("ttl.sh/myapp:latest")
return addr
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Build and publish Docker container
*/
@func()
build(src: Directory): Promise<string> {
// build app
const builder = dag
.container()
.from("golang:latest")
.withDirectory("/src", src)
.withWorkdir("/src")
.withEnvVariable("CGO_ENABLED", "0")
.withExec(["go", "build", "-o", "myapp"])
// publish binary on alpine base
const prodImage = dag
.container()
.from("alpine")
.withFile("/bin/myapp", builder.file("/src/myapp"))
.withEntrypoint(["/bin/myapp"])
// publish to ttl.sh registry
const addr = prodImage.publish("ttl.sh/myapp:latest")
return addr
}
}
Example
Perform a multi-stage build of the source code in the golang/example/hello
repository and publish the resulting image:
dagger call build --src="https://github.com/golang/example#master:hello"
Perform a matrix build
The following Dagger Function performs a matrix build.
- Go
- Python
- TypeScript
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
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def build(self, src: dagger.Directory) -> dagger.Directory:
"""Build and return directory of go binaries"""
# define build matrix
gooses = ["linux", "darwin"]
goarches = ["amd64", "arm64"]
# create empty directory to put build artifacts
outputs = dag.directory()
golang = (
dag.container()
.from_("golang:latest")
.with_directory("/src", src)
.with_workdir("/src")
)
for goos in gooses:
for goarch in goarches:
# create directory for each OS and architecture
path = f"build/{goos}/{goarch}/"
# build artifact
build = (
golang.with_env_variable("GOOS", goos)
.with_env_variable("GOARCH", goarch)
.with_exec(["go", "build", "-o", path])
)
# add build to outputs
outputs = outputs.with_directory(path, build.directory(path))
return await outputs
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Build and return directory of go binaries
*/
@func()
build(src: Directory): Directory {
// define build matrix
const gooses = ["linux", "darwin"]
const goarches = ["amd64", "arm64"]
// create empty directory to put build artifacts
let outputs = dag.directory()
const golang = dag
.container()
.from("golang:latest")
.withDirectory("/src", src)
.withWorkdir("/src")
for (const goos of gooses) {
for (const goarch of goarches) {
// create a directory for each OS and architecture
const path = `build/${goos}/${goarch}/`
// build artifact
const build = golang
.withEnvVariable("GOOS", goos)
.withEnvVariable("GOARCH", goarch)
.withExec(["go", "build", "-o", path])
// add build to outputs
outputs = outputs.withDirectory(path, build.directory(path))
}
}
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 call build \
--src="https://github.com/golang/example#master:hello" \
export --path /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.
- Go
- Python
- TypeScript
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
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
async def build(
self,
src: Annotated[
dagger.Directory,
Doc(
"Source code location can be local directory or remote Git \
repository"
),
],
) -> str:
"""Build and publish multi-platform image"""
# platforms to build for and push in a multi-platform image
platforms = [
dagger.Platform("linux/amd64"), # a.k.a. x86_64
dagger.Platform("linux/arm64"), # a.k.a. aarch64
dagger.Platform("linux/s390x"), # a.k.a. IBM S/390
]
# container registry for multi-platform image
image_repo = "ttl.sh/myapp:latest"
platform_variants = []
for platform in platforms:
# pull golang image for this platform
ctr = (
dag.container(platform=platform)
.from_("golang:1.20-alpine")
# mount source
.with_directory("/src", src)
# mount empty dir where built binary will live
.with_directory("/output", dag.directory())
# ensure binary will be statically linked and thus executable
# in the final image
.with_env_variable("CGO_ENABLED", "0")
# build binary and put result at mounted output directory
.with_workdir("/src")
.with_exec(["go", "build", "-o", "/output/hello"])
)
# select output directory
output_dir = ctr.directory("/output")
# wrap output directory in a new empty container marked
# with the same platform
binary_ctr = dag.container(platform=platform).with_rootfs(output_dir)
platform_variants.append(binary_ctr)
# publish to registry
image_digest = dag.container().publish(
image_repo, platform_variants=platform_variants
)
return await image_digest
import {
dag,
Container,
Directory,
Platform,
object,
func,
} from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Build and publish multi-platform image
* @param src source code location
*/
@func()
async build(src: Directory): Promise<string> {
// platforms to build for and push in a multi-platform image
const platforms: Platform[] = [
"linux/amd64" as Platform, // a.k.a. x86_64
"linux/arm64" as Platform, // a.k.a. aarch64
"linux/s390x" as Platform, // a.k.a. IBM S/390
]
// container registry for multi-platform image
const imageRepo = "ttl.sh/myapp:latest"
const platformVariants: Array<Container> = []
for (const platform of platforms) {
const ctr = dag
.container({ platform: platform })
.from("golang:1.21-alpine")
// mount source
.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")
.withWorkdir("/src")
.withExec(["go", "build", "-o", "/output/hello"])
// select output directory
const outputDir = ctr.directory("/output")
// wrap output directory in a new empty container marked
// with the same platform
const binaryCtr = await dag
.container({ platform: platform })
.withRootfs(outputDir)
platformVariants.push(binaryCtr)
}
// publish to registry
const imageDigest = await dag
.container()
.publish(imageRepo, { platformVariants: platformVariants })
return imageDigest
}
}
Example
Build and publish a multi-platform image:
dagger call build --src="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.
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
- Go
- Python
- TypeScript
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
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
async def build(
self,
src: Annotated[
dagger.Directory,
Doc(
"Source code location can be local directory or remote Git \
repository"
),
],
) -> str:
"""Build an publish multi-platform image"""
# platforms to build for and push in a multi-platform image
platforms = [
dagger.Platform("linux/amd64"), # a.k.a. x86_64
dagger.Platform("linux/arm64"), # a.k.a. aarch64
dagger.Platform("linux/s390x"), # a.k.a. IBM S/390
]
# container registry for multi-platform image
image_repo = "ttl.sh/myapp:latest"
platform_variants = []
for platform in platforms:
# parse architecture using containerd utility module
platform_arch = await dag.containerd().architecture_of(platform)
# 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
.with_directory("/src", src)
# mount empty dir where built binary will live
.with_directory("/output", dag.directory())
# ensure binary will be statically linked and thus executable
# in the final image
.with_env_variable("CGO_ENABLED", "0")
# configure go compiler to use cross-compilation targeting the
# desired platform
.with_env_variable("GOOS", "linux")
.with_env_variable("GOARCH", platform_arch)
# build binary and put result at mounted output directory
.with_workdir("/src")
.with_exec(["go", "build", "-o", "/output/hello"])
)
# selelct output directory
output_dir = ctr.directory("/output")
# wrap output directory in a new empty container marked
# with the same platform
binary_ctr = (
dag.container(platform=platform)
.with_rootfs(output_dir)
.with_entrypoint(["/hello"])
)
platform_variants.append(binary_ctr)
# publish to registry
image_digest = dag.container().publish(
image_repo, platform_variants=platform_variants
)
return await image_digest
import {
dag,
Container,
Directory,
Platform,
object,
func,
} from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Build and publish multi-platform image
* @param src source code location
*/
@func()
async build(src: Directory): Promise<string> {
// platforms to build for and push in a multi-platform image
const platforms: Platform[] = [
"linux/amd64" as Platform, // a.k.a. x86_64
"linux/arm64" as Platform, // a.k.a. aarch64
"linux/s390x" as Platform, // a.k.a. IBM S/390
]
// container registry for multi-platform image
const imageRepo = "ttl.sh/myapp:latest"
const platformVariants: Array<Container> = []
for (const platform of platforms) {
// parse architecture using containerd utility module
const platformArch = await dag.containerd().architectureOf(platform)
const ctr = dag
// pull golang image for the *host* platform, this is done by
// not specifying the a platform. The default is the host platform.
.container()
.from("golang:1.21-alpine")
// mount source
.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)
.withWorkdir("/src")
.withExec(["go", "build", "-o", "/output/hello"])
// select output directory
const outputDir = ctr.directory("/output")
// wrap output directory in a new empty container marked
// with the same platform
const binaryCtr = await dag
.container({ platform: platform })
.withRootfs(outputDir)
.withEntrypoint(["/hello"])
platformVariants.push(binaryCtr)
}
// publish to registry
const imageDigest = await dag
.container()
.publish(imageRepo, { platformVariants: platformVariants })
return imageDigest
}
}
Example
Build and publish a multi-platform image with cross compliation:
dagger call build --src="https://github.com/golang/example#master:hello"
Build image from Dockerfile
The following Dagger Function builds an image from a Dockerfile.
- Go
- Python
- TypeScript
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 := dag.Container().
WithDirectory("/src", src).
WithWorkdir("/src").
Directory("/src").
DockerBuild(). // build from Dockerfile
Publish(ctx, "ttl.sh/hello-dagger")
if err != nil {
return "", err
}
return ref, nil
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
async def build(
self,
src: Annotated[
dagger.Directory,
Doc("location of directory containing Dockerfile"),
],
) -> str:
"""Build and publish image from existing Dockerfile"""
ref = (
dag.container()
.with_directory("/src", src)
.with_workdir("/src")
.directory("/src")
.docker_build() # build from Dockerfile
.publish("ttl.sh/hello-dagger")
)
return await ref
import { dag, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Build and publish image from existing Dockerfile
* @param src location of directory containing Dockerfile
*/
@func()
async build(src: Directory): Promise<string> {
const ref = await dag
.container()
.withDirectory("/src", src)
.withWorkdir("/src")
.directory("/src")
.dockerBuild() // build from Dockerfile
.publish("ttl.sh/hello-dagger")
return ref
}
}
Example
Build and publish an image from an existing Dockerfile
dagger call build --src 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.
- Go
- Python
- TypeScript
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
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
async def build(
self,
src: Annotated[
dagger.Directory,
Doc("location of source directory"),
],
dockerfile: Annotated[
dagger.File,
Doc("location of Dockerfile"),
],
) -> str:
"""
Build and publish image from Dockerfile
This example uses a build context directory in a different location
than the current working directory.
"""
# get build context with dockerfile added
workspace = (
dag.container()
.with_directory("/src", src)
.with_workdir("/src")
.with_file("/src/custom.Dockerfile", dockerfile)
.directory("/src")
)
# build using Dockerfile and publish to registry
ref = (
dag.container()
.build(context=workspace, dockerfile="custom.Dockerfile")
.publish("ttl.sh/hello-dagger")
)
return await ref
import { dag, Directory, File, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Build and publish image from existing Dockerfile. This example uses a
* build context directory in a different location than the current working
* directory.
* @param src location of source directory
* @param dockerfile location of dockerfile
*/
@func()
async build(src: Directory, dockerfile: File): Promise<string> {
// get build context with Dockerfile added
const workspace = await dag
.container()
.withDirectory("/src", src)
.withWorkdir("/src")
.withFile("/src/custom.Dockerfile", dockerfile)
.directory("/src")
// build using Dockerfile and publish to registry
const ref = await dag
.container()
.build(workspace, { dockerfile: "custom.Dockerfile" })
.publish("ttl.sh/hello-dagger")
return ref
}
}
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 call build \
--src "https://github.com/dockersamples/python-flask-redis" \
--dockerfile https://github.com/vimagick/dockerfiles#master:registry-cli/Dockerfile
Add OCI labels to image
The following Dagger Function adds OpenContainer Initiative (OCI) labels to an image.
- Go
- Python
- TypeScript
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
}
from datetime import datetime, timezone
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def build(self) -> str:
"""Build and publish image with oci labels"""
ref = (
dag.container()
.from_("alpine")
.with_label("org.opencontainers.image.title", "my-alpine")
.with_label("org.opencontainers.image.version", "1.0")
.with_label(
"org.opencontainers.image.created",
datetime.now(timezone.utc).isoformat(),
)
.with_label(
"org.opencontainers.image.source",
"https://github.com/alpinelinux/docker-alpine",
)
.with_label("org.opencontainers.image.licenses", "MIT")
.publish("ttl.sh/my-alpine")
)
return await ref
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Build and publish image with oci labels
*/
@func()
async build(): Promise<string> {
const ref = await dag
.container()
.from("alpine")
.withLabel("org.opencontainers.image.title", "my-alpine")
.withLabel("org.opencontainers.image.version", "1.0")
.withLabel("org.opencontainers.image.created", new Date())
.withLabel(
"org.opencontainers.image.source",
"https://github.com/alpinelinux/docker-alpine",
)
.withLabel("org.opencontainers.image.licenses", "MIT")
.publish("ttl.sh/hello-dagger")
return ref
}
}
Example
Build and publish an image with OCI labels:
dagger call build
Invalidate cache
The following function demonstrates how to invalidate the Dagger pipeline operations cache and force execution of subsequent pipeline steps, by introducing a volatile time variable at a specific point in the Dagger pipeline.
- 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 pipeline operations cache.
- Go
- Python
- TypeScript
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
}
from datetime import datetime
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def build(self) -> str:
"""Run a build with cache invalidation"""
output = (
dag.container()
.from_("alpine")
# comment out the line below to see the cached date output
.with_env_variable("CACHEBUSTER", str(datetime.now()))
.with_exec(["date"])
.stdout()
)
return await output
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Run a build with cache invalidation
*/
@func()
async build(): Promise<string> {
const ref = await dag
.container()
.from("alpine")
// comment out the line below to see the cached date output
.withEnvVariable("CACHEBUSTER", Date.now().toString())
.withExec(["date"])
.stdout()
return ref
}
}
Example
Execute a build, invalidating the cache on each run:
dagger call build
Secrets
Use secret variables
The following Dagger Function accepts a GitHub personal access token as a Secret
, and uses the Secret
to authorize a request to the GitHub API. The secret may be sourced from a host environment variable, host file, or host command execution.
- Go
- Python
- TypeScript
package main
import (
"context"
"main/internal/dagger"
)
type MyModule struct{}
// Query the GitHub API
func (m *MyModule) GithubApi(
ctx context.Context,
// GitHub API token
token *dagger.Secret,
) (string, error) {
return dag.Container().
From("alpine:3.17").
WithSecretVariable("GITHUB_API_TOKEN", token).
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)
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
async def github_api(
self,
token: Annotated[dagger.Secret, Doc("GitHub API token")],
) -> str:
"""Query the GitHub API"""
return await (
dag.container(platform=dagger.Platform("linux/amd64"))
.from_("alpine:3.17")
.with_secret_variable("GITHUB_API_TOKEN", token)
.with_exec(["apk", "add", "curl"])
.with_exec(
[
"sh",
"-c",
"""curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN" """,
]
)
.stdout()
)
import { dag, object, func, Secret } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Query the GitHub API
*/
@func()
async githubApi(
/**
* GitHub API token
*/
token: Secret,
): Promise<string> {
return await dag
.container()
.from("alpine:3.17")
.withSecretVariable("GITHUB_API_TOKEN", token)
.withExec(["apk", "add", "curl"])
.withExec([
"sh",
"-c",
`curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN"`,
])
.stdout()
}
}
Examples
-
Query the API using a secret sourced from an environment variable:
dagger call github-api --token=env:GITHUB_API_TOKEN
-
Query the API using a secret sourced from a file:
dagger call github-api --token=file:./github.txt
-
Query the API using a secret sourced from a command:
dagger call github-api --token=cmd:"gh auth token"
Mount files as secrets
The following Dagger Function accepts a GitHub hosts configuration file as a Secret
, and mounts the file as a Secret
to a container to authorize a request to GitHub.
- Go
- Python
- TypeScript
package main
import (
"context"
"main/internal/dagger"
)
type MyModule struct{}
// Query the GitHub API
func (m *MyModule) GithubAuth(
ctx context.Context,
// GitHub Hosts configuration file
ghCreds *dagger.Secret,
) (string, error) {
return dag.Container().
From("alpine:3.17").
WithExec([]string{"apk", "add", "github-cli"}).
WithMountedSecret("/root/.config/gh/hosts.yml", ghCreds).
WithWorkdir("/root").
WithExec([]string{"gh", "auth", "status"}).
Stdout(ctx)
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
async def github_auth(
self,
gh_creds: Annotated[dagger.Secret, Doc("GitHub Hosts configuration file")],
) -> str:
"""Query the GitHub API"""
return await (
dag.container()
.from_("alpine:3.17")
.with_exec(["apk", "add", "github-cli"])
.with_mounted_secret("/root/.config/gh/hosts.yml", gh_creds)
.with_workdir("/root")
.with_exec(["gh", "auth", "status"])
.stdout()
)
import { dag, object, func, Secret } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Query the GitHub API
*/
@func()
async githubAuth(
/**
* GitHub Hosts configuration File
*/
ghCreds: Secret,
): Promise<string> {
return await dag
.container()
.from("alpine:3.17")
.withExec(["apk", "add", "github-cli"])
.withMountedSecret("/root/.config/gh/hosts.yml", ghCreds)
.withExec(["gh", "auth", "status"])
.stdout()
}
}
Example
Query the GitHub API using a mounted secret file:
dagger call github-auth \
--gh-creds=file:$HOME/.config/gh/hosts.yml
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
.
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger.io/dagger"
)
type MyModule struct{}
// Build a Container from a Dockerfile
func (m *MyModule) Build(
ctx context.Context,
// The source code to build
source *dagger.Directory,
// The secret to use in the Dockerfile
secret *dagger.Secret,
) (*dagger.Container, error) {
secretName, err := secret.Name(ctx)
if err != nil {
return nil, err
}
return source.
DockerBuild(dagger.DirectoryDockerBuildOpts{
Dockerfile: "Dockerfile",
BuildArgs: []dagger.BuildArg{
{Name: "gh-secret", Value: secretName},
},
Secrets: []*dagger.Secret{secret},
}), nil
}
from typing import Annotated
import dagger
from dagger import Doc, function, object_type
@object_type
class MyModule:
@function
async def build(
self,
source: Annotated[dagger.Directory, Doc("The source code to build")],
secret: Annotated[dagger.Secret, Doc("The secret to use in the Dockerfile")],
) -> dagger.Container:
"""Build a Container from a Dockerfile"""
secret_name = await secret.name()
return source.docker_build(
dockerfile="Dockerfile",
build_args=dagger.DockerBuildArgs(name="gh-secret", value=secret_name),
secrets=[secret],
)
import { object, func, Secret } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Build a Container from a Dockerfile
*/
@func()
async build(
/**
* The source code to build
*/
source: Directory,
/**
* The secret to use in the Dockerfile
*/
secret: Secret,
): Promise<Container> {
const secretName = await secret.name()
return source.dockerBuild({
dockerfile: "Dockerfile",
buildArgs: [{ name: "gh-secret", value: secretName }],
secrets: [secret],
})
}
}
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)"
Example
Build from a Dockerfile with a mounted secret:
dagger call build --source=. --secret=env:GITHUB_API_TOKEN
Services
Bind and use services in Dagger Functions
The first Dagger Function below creates and returns an HTTP service. This service is bound and used from a different Dagger Function, via a service binding using an alias like www
.
- Go
- Python
- TypeScript
package main
import (
"context"
"main/internal/dagger"
)
type MyModule struct{}
// Start and return an HTTP service
func (m *MyModule) HttpService() *dagger.Service {
return dag.Container().
From("python").
WithWorkdir("/srv").
WithNewFile("index.html", "Hello, world!").
WithExec([]string{"python", "-m", "http.server", "8080"}).
WithExposedPort(8080).
AsService()
}
// Send a request to an HTTP service and return the response
func (m *MyModule) Get(ctx context.Context) (string, error) {
return dag.Container().
From("alpine").
WithServiceBinding("www", m.HttpService()).
WithExec([]string{"wget", "-O-", "http://www:8080"}).
Stdout(ctx)
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def http_service(self) -> dagger.Service:
"""Start and return an HTTP service."""
return (
dag.container()
.from_("python")
.with_workdir("/srv")
.with_new_file("index.html", "Hello, world!")
.with_exec(["python", "-m", "http.server", "8080"])
.with_exposed_port(8080)
.as_service()
)
@function
async def get(self) -> str:
"""Send a request to an HTTP service and return the response."""
return await (
dag.container()
.from_("alpine")
.with_service_binding("www", self.http_service())
.with_exec(["wget", "-O-", "http://www:8080"])
.stdout()
)
import { dag, object, func, Service } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Start and return an HTTP service
*/
@func()
httpService(): Service {
return dag
.container()
.from("python")
.withWorkdir("/srv")
.withNewFile("index.html", "Hello, world!")
.withExec(["python", "-m", "http.server", "8080"])
.withExposedPort(8080)
.asService()
}
/**
* Send a request to an HTTP service and return the response
*/
@func()
async get(): Promise<string> {
return await dag
.container()
.from("alpine")
.withServiceBinding("www", this.httpService())
.withExec(["wget", "-O-", "http://www:8080"])
.stdout()
}
}
Example
Send a request from one Dagger Function to a bound HTTP service instantiated by a different Dagger Function:
dagger call get
Expose services in Dagger Functions to the host
The Dagger Function below creates and returns an HTTP service. This service can be used from the host.
- Go
- Python
- TypeScript
package main
import "main/internal/dagger"
type MyModule struct{}
// Start and return an HTTP service
func (m *MyModule) HttpService() *dagger.Service {
return dag.Container().
From("python").
WithWorkdir("/srv").
WithNewFile("index.html", "Hello, world!").
WithExec([]string{"python", "-m", "http.server", "8080"}).
WithExposedPort(8080).
AsService()
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def http_service(self) -> dagger.Service:
"""Start and return an HTTP service."""
return (
dag.container()
.from_("python")
.with_workdir("/srv")
.with_new_file("index.html", "Hello, world!")
.with_exec(["python", "-m", "http.server", "8080"])
.with_exposed_port(8080)
.as_service()
)
import { dag, object, func, Service } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Start and return an HTTP service
*/
@func()
httpService(): Service {
return dag
.container()
.from("python")
.withWorkdir("/srv")
.withNewFile("index.html", "Hello, world!")
.withExec(["python", "-m", "http.server", "8080"])
.withExposedPort(8080)
.asService()
}
}
Examples
-
Expose the HTTP service instantiated by a Dagger Function to the host on the default port:
dagger call http-service up
# access the service from the host
curl localhost:8080 -
Expose the HTTP service instantiated by a Dagger Function to the host on a different host port:
dagger call \
http-service \
up --ports 9000:8080
# access the service from the host
curl localhost:9000
Expose host services to Dagger Functions
The following Dagger Function accepts a Service
running on the host, binds it using an alias, and creates a client to access it via the service binding. This example uses a MariaDB database service running on host port 3306, aliased as db
in the Dagger Function.
This implies that a service is already listening on a port on the host, out-of-band of Dagger.
- Go
- Python
- TypeScript
package main
import (
"context"
"main/internal/dagger"
)
type MyModule struct{}
// Send a query to a MariaDB service and return the response
func (m *MyModule) UserList(
ctx context.Context,
// Host service
svc *dagger.Service,
) (string, error) {
return dag.Container().
From("mariadb:10.11.2").
WithServiceBinding("db", svc).
WithExec([]string{"/usr/bin/mysql", "--user=root", "--password=secret", "--host=db", "-e", "SELECT Host, User FROM mysql.user"}).
Stdout(ctx)
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
async def user_list(
self, svc: Annotated[dagger.Service, Doc("Host service")]
) -> str:
"""Send a query to a MariaDB service and return the response."""
return await (
dag.container()
.from_("mariadb:10.11.2")
.with_service_binding("db", svc)
.with_exec(
[
"/usr/bin/mysql",
"--user=root",
"--password=secret",
"--host=db",
"-e",
"SELECT Host, User FROM mysql.user",
]
)
.stdout()
)
import { dag, object, func, Service } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Send a query to a MariaDB service and returns the response
*/
@func()
async userList(
/**
* Host service
*/
svc: Service,
): Promise<string> {
return await dag
.container()
.from("mariadb:10.11.2")
.withServiceBinding("db", svc)
.withExec([
"/usr/bin/mysql",
"--user=root",
"--password=secret",
"--host=db",
"-e",
"SELECT Host, User FROM mysql.user",
])
.stdout()
}
}
Example
Send a query to the database service listening on host port 3306 and return the result as a string:
# assumes a service is listening on host port 3306
dagger call user-list --svc=tcp://localhost:3306
Create a transient service for unit tests
The following Dagger Function creates a service and binds it to an application container for unit testing. In this example, the application being tested is Drupal. Drupal includes a large number of unit tests, including tests which depend on an active database service. This database service is created on-the-fly by the Dagger Function.
- Go
- Python
- TypeScript
package main
import (
"context"
)
type MyModule struct{}
// Run unit tests against a database service
func (m *MyModule) Test(ctx context.Context) (string, error) {
mariadb := dag.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 := dag.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
return 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)
}
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def test(self) -> str:
"""Run unit tests against a database service."""
# get MariaDB base image
mariadb = (
dag.container()
.from_("mariadb:10.11.2")
.with_env_variable("MARIADB_USER", "user")
.with_env_variable("MARIADB_PASSWORD", "password")
.with_env_variable("MARIADB_DATABASE", "drupal")
.with_env_variable("MARIADB_ROOT_PASSWORD", "root")
.with_exposed_port(3306)
.as_service()
)
# get Drupal base image
# install additional dependencies
drupal = (
dag.container()
.from_("drupal:10.0.7-php8.2-fpm")
.with_exec(
[
"composer",
"require",
"drupal/core-dev",
"--dev",
"--update-with-all-dependencies",
]
)
)
# add service binding for MariaDB
# run kernel tests using PHPUnit
return await (
drupal.with_service_binding("db", mariadb)
.with_env_variable("SIMPLETEST_DB", "mysql://user:password@db/drupal")
.with_env_variable("SYMFONY_DEPRECATIONS_HELPER", "disabled")
.with_workdir("/opt/drupal/web/core")
.with_exec(["../../vendor/bin/phpunit", "-v", "--group", "KernelTests"])
.stdout()
)
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Run unit tests against a database service
*/
@func()
async test(): Promise<string> {
const mariadb = dag
.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
const drupal = dag
.container()
.from("drupal:10.0.7-php8.2-fpm")
.withExec([
"composer",
"require",
"drupal/core-dev",
"--dev",
"--update-with-all-dependencies",
])
// add service binding for MariaDB
// run kernel tests using PHPUnit
return await drupal
.withServiceBinding("db", mariadb)
.withEnvVariable("SIMPLETEST_DB", "mysql://user:password@db/drupal")
.withEnvVariable("SYMFONY_DEPRECATIONS_HELPER", "disabled")
.withWorkdir("/opt/drupal/web/core")
.withExec(["../../vendor/bin/phpunit", "-v", "--group", "KernelTests"])
.stdout()
}
}
Example
Run Drupal's unit tests, instantiating a database service during the process:
dagger call test
Start and stop services
The following Dagger Function demonstrates how to control a service's lifecycle by explicitly starting and stopping a service. This example uses a Redis service.
- Go
- Python
- TypeScript
package main
import (
"context"
)
type MyModule struct{}
// Explicitly start and stop a Redis service
func (m *MyModule) RedisService(ctx context.Context) (string, error) {
redisSrv := dag.Container().
From("redis").
WithExposedPort(6379).
AsService()
// start Redis ahead of time so it stays up for the duration of the test
redisSrv, err := redisSrv.Start(ctx)
if err != nil {
return "", err
}
// stop the service when done
defer redisSrv.Stop(ctx)
// create Redis client container
redisCLI := dag.Container().
From("redis").
WithServiceBinding("redis-srv", redisSrv).
WithEntrypoint([]string{"redis-cli", "-h", "redis-srv"})
// set value
setter, err := redisCLI.
WithExec([]string{"set", "foo", "abc"}).
Stdout(ctx)
if err != nil {
return "", err
}
// get value
getter, err := redisCLI.
WithExec([]string{"get", "foo"}).
Stdout(ctx)
if err != nil {
return "", err
}
return setter + getter, nil
}
import contextlib
import dagger
from dagger import dag, function, object_type
@contextlib.asynccontextmanager
async def managed_service(svc: dagger.Service):
"""Start and stop a service."""
yield await svc.start()
await svc.stop()
@object_type
class MyModule:
@function
async def redis_service(self) -> str:
"""Explicitly start and stop a Redis service."""
redis_srv = dag.container().from_("redis").with_exposed_port(6379).as_service()
# start Redis ahead of time so it stays up for the duration of the test
# and stop when done
async with managed_service(redis_srv) as redis_srv:
# create Redis client container
redis_cli = (
dag.container()
.from_("redis")
.with_service_binding("redis-srv", redis_srv)
.with_entrypoint(["redis-cli", "-h", "redis-srv"])
)
# set value
setter = await redis_cli.with_exec(["set", "foo", "abc"]).stdout()
# get value
getter = await redis_cli.with_exec(["get", "foo"]).stdout()
return setter + getter
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Explicitly start and stop a Redis service
*/
@func()
async redisService(): Promise<string> {
let redisSrv = dag
.container()
.from("redis")
.withExposedPort(6379)
.asService()
// start Redis ahead of time so it stays up for the duration of the test
redisSrv = await redisSrv.start()
// stop the service when done
await redisSrv.stop()
// create Redis client container
const redisCLI = dag
.container()
.from("redis")
.withServiceBinding("redis-srv", redisSrv)
.withEntrypoint(["redis-cli", "-h", "redis-srv"])
// set value
const setter = await redisCLI.withExec(["set", "foo", "abc"]).stdout()
// get value
const getter = await redisCLI.withExec(["get", "foo"]).stdout()
return setter + getter
}
}
Example
Start and stop a Redis service:
dagger call redis-service
Just-in-time artifacts
Publish a container image to a private registry
The following Dagger Function publishes a just-in-time container image to a private registry.
- Go
- Python
- TypeScript
package main
import (
"context"
"fmt"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Publish a container image to a private registry
func (m *MyModule) Publish(
ctx context.Context,
// Registry address
registry string,
// Registry username
username string,
// Registry password
password *dagger.Secret,
) (string, error) {
return dag.Container().
From("nginx:1.23-alpine").
WithNewFile(
"/usr/share/nginx/html/index.html",
"Hello from Dagger!",
dagger.ContainerWithNewFileOpts{Permissions: 0o400},
).
WithRegistryAuth(registry, username, password).
Publish(ctx, fmt.Sprintf("%s/%s/my-nginx", registry, username))
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
async def publish(
self,
registry: Annotated[str, Doc("Registry address")],
username: Annotated[str, Doc("Registry username")],
password: Annotated[dagger.Secret, Doc("Registry password")],
) -> str:
"""Publish a container image to a private registry"""
return await (
dag.container()
.from_("nginx:1.23-alpine")
.with_new_file(
"/usr/share/nginx/html/index.html",
"Hello from Dagger!",
permissions=0o400,
)
.with_registry_auth(registry, username, password)
.publish(f"{registry}/{username}/my-nginx")
)
import { dag, Secret, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Publish a container image to a private registry
*/
@func()
async publish(
/**
* Registry address
*/
registry: string,
/**
* Registry username
*/
username: string,
/**
* Registry password
*/
password: Secret,
): Promise<string> {
return await dag
.container()
.from("nginx:1.23-alpine")
.withNewFile("/usr/share/nginx/html/index.html", "Hello from Dagger!", {
permissions: 0o400,
})
.withRegistryAuth(registry, username, password)
.publish(`${registry}/${username}/my-nginx`)
}
}
Examples
-
Publish a just-in-time container image to Docker Hub, using the account username
user
and the password set in thePASSWORD
environment variable:dagger call publish --registry=docker.io --username=user --password=env:PASSWORD
-
Publish a just-in-time container image to GitHub Container Registry, using the account username
user
and the GitHub personal access token set in thePASSWORD
environment variable:dagger call publish --registry=ghcr.io --username=user --password=env:PASSWORD
Publish a container image to a private registry with multiple tags
The following Dagger Function tags a just-in-time container image multiple times and publishes it to a private registry.
- Go
- Python
- TypeScript
package main
import (
"context"
"fmt"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Tag a container image multiple times and publish it to a private registry
func (m *MyModule) Publish(
ctx context.Context,
// Registry address
registry string,
// Registry username
username string,
// Registry password
password *dagger.Secret,
) ([]string, error) {
tags := [4]string{"latest", "1.0-alpine", "1.0", "1.0.0"}
addr := []string{}
ctr := dag.Container().
From("nginx:1.23-alpine").
WithNewFile(
"/usr/share/nginx/html/index.html",
"Hello from Dagger!",
dagger.ContainerWithNewFileOpts{Permissions: 0o400},
).
WithRegistryAuth(registry, username, password)
for _, tag := range tags {
a, err := ctr.Publish(ctx, fmt.Sprintf("%s/%s/my-nginx:%s", registry, username, tag))
if err != nil {
return addr, err
}
addr = append(addr, a)
}
return addr, nil
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
async def publish(
self,
registry: Annotated[str, Doc("Registry address")],
username: Annotated[str, Doc("Registry username")],
password: Annotated[dagger.Secret, Doc("Registry password")],
) -> list[str]:
"""Tag a container image multiple times and publish it to a private registry"""
tags = ["latest", "1.0-alpine", "1.0", "1.0.0"]
addr = []
container = (
dag.container()
.from_("nginx:1.23-alpine")
.with_new_file(
"/usr/share/nginx/html/index.html",
"Hello from Dagger!",
permissions=0o400,
)
.with_registry_auth(registry, username, password)
)
for tag in tags:
a = await container.publish(f"{registry}/{username}/my-nginx:{tag}")
addr.append(a)
return addr
import { dag, Secret, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Tag a container image multiple times and publish it to a private registry
*/
@func()
async publish(
/**
* Registry address
*/
registry: string,
/**
* Registry username
*/
username: string,
/**
* Registry password
*/
password: Secret,
): Promise<string[]> {
const tags = ["latest", "1.0-alpine", "1.0", "1.0.0"]
const addr: string[] = []
const container = dag
.container()
.from("nginx:1.23-alpine")
.withNewFile("/usr/share/nginx/html/index.html", "Hello from Dagger!", {
permissions: 0o400,
})
.withRegistryAuth(registry, username, password)
for (const tag in tags) {
const a = await container.publish(
`${registry}/${username}/my-nginx:${tags[tag]}`,
)
addr.push(a)
}
return addr
}
}
Examples
-
Tag and publish a just-in-time container image to Docker Hub, using the account username
user
and the password set in thePASSWORD
environment variable:dagger call publish --registry=docker.io --username=user --password=env:PASSWORD
-
Tag and publish a just-in-time container image to GitHub Container Registry, using the account username
user
and the GitHub personal access token set in thePASSWORD
environment variable:dagger call publish --registry=ghcr.io --username=user --password=env:PASSWORD
Export a directory or file to the host
The following Dagger Functions return a just-in-time directory and file. These outputs can be exported to the host with dagger call ... export ...
.
- Go
- Python
- TypeScript
package main
import "dagger/my-module/internal/dagger"
type MyModule struct{}
// Return a directory
func (m *MyModule) GetDir() *dagger.Directory {
return m.Base().
Directory("/src")
}
// Return a file
func (m *MyModule) GetFile() *dagger.File {
return m.Base().
File("/src/foo")
}
// Return a base container
func (m *MyModule) Base() *dagger.Container {
return dag.Container().
From("alpine:latest").
WithExec([]string{"mkdir", "/src"}).
WithExec([]string{"touch", "/src/foo", "/src/bar"})
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def get_dir(self) -> dagger.Directory:
"""Return a directory"""
return self.base().directory("/src")
@function
def get_file(self) -> dagger.File:
"""Return a file"""
return self.base().file("/src/foo")
@function
def base(self) -> dagger.Container:
"""Return a base container"""
return (
dag.container()
.from_("alpine:latest")
.with_exec(["mkdir", "/src"])
.with_exec(["touch", "/src/foo", "/src/bar"])
)
import {
dag,
Directory,
Container,
File,
object,
func,
} from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Return a directory
*/
@func()
getDir(): Directory {
return this.base().directory("/src")
}
/**
* Return a file
*/
@func()
getFile(): File {
return this.base().file("/src/foo")
}
/**
* Return a base container
*/
@func()
base(): Container {
return dag
.container()
.from("alpine:latest")
.withExec(["mkdir", "/src"])
.withExec(["touch", "/src/foo", "/src/bar"])
}
}
Examples
-
Export the directory returned by the Dagger Function to the
/home/admin/export
path on the host:dagger call \
get-dir \
export --path=/home/admin/export -
Export the file returned by the Dagger Function to the
/home/admin/myfile
path on the host:dagger call \
get-file \
export --path=/home/admin/myfile
Export a container image to the host
The following Dagger Function returns a just-in-time container. This can be exported to the host as an OCI tarball with dagger call ... export ...
.
- Go
- Python
- TypeScript
package main
import "dagger/my-module/internal/dagger"
type MyModule struct{}
// Return a container
func (m *MyModule) Base() *dagger.Container {
return dag.Container().
From("alpine:latest").
WithExec([]string{"mkdir", "/src"}).
WithExec([]string{"touch", "/src/foo", "/src/bar"})
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def base(self) -> dagger.Container:
"""Return a container"""
return (
dag.container()
.from_("alpine:latest")
.with_exec(["mkdir", "/src"])
.with_exec(["touch", "/src/foo", "/src/bar"])
)
import { dag, Container, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Return a container
*/
@func()
base(): Container {
return dag
.container()
.from("alpine:latest")
.withExec(["mkdir", "/src"])
.withExec(["touch", "/src/foo", "/src/bar"])
}
}
Example
Export the container image returned by the Dagger Function as an OCI tarball to the /home/admin/mycontainer.tgz
path on the host. This OCI tarball can then be loaded into Docker with docker load ...
.
dagger call \
base \
export --path=/home/admin/mycontainer.tgz
Optimizations
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.
- Go
- Python
- TypeScript
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"})
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
def build(
self, source: Annotated[dagger.Directory, Doc("Source code location")]
) -> dagger.Container:
"""Build an application using cached dependencies"""
return (
dag.container()
.from_("python:3.11")
.with_directory("/src", source)
.with_workdir("/src")
.with_mounted_cache("/root/.cache/pip", dag.cache_volume("python-311"))
.with_exec(["pip", "install", "-r", "requirements.txt"])
)
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Build an application using cached dependencies
*/
@func()
build(
/**
* Source code location
*/
source: Directory,
): Container {
return dag
.container()
.from("node:21")
.withDirectory("/src", source)
.withWorkdir("/src")
.withMountedCache(
"/src/node_modules",
dag.cacheVolume("node-21-myapp-myenv"),
)
.withMountedCache("/root/.npm", dag.cacheVolume("node-21"))
.withExec(["npm", "install"])
}
}
Example
Build an application using cached dependencies:
dagger call build --source=.
Set environment variables in a container
The following Dagger Function demonstrates how to set multiple environment variables in a container.
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
type EnvVar struct {
Name string
Value string
}
// Set environment variables in a container
func (m *MyModule) SetEnv(ctx context.Context) (string, error) {
return dag.Container().
From("alpine").
With(EnvVariables([]*EnvVar{
{"ENV_VAR_1", "VALUE 1"},
{"ENV_VAR_2", "VALUE 2"},
{"ENV_VAR_3", "VALUE 3"},
})).
WithExec([]string{"env"}).
Stdout(ctx)
}
func EnvVariables(envs []*EnvVar) dagger.WithContainerFunc {
return func(c *dagger.Container) *dagger.Container {
for _, e := range envs {
c = c.WithEnvVariable(e.Name, e.Value)
}
return c
}
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def set_env(self) -> str:
"""Set environment variables in a container"""
return await (
dag.container()
.from_("alpine")
.with_(
self.env_variables(
[
("ENV_VAR_1", "VALUE 1"),
("ENV_VAR_2", "VALUE 2"),
("ENV_VAR_3", "VALUE 3"),
]
)
)
.with_exec(["env"])
.stdout()
)
def env_variables(self, envs: list[tuple[str, str]]):
def env_variables_inner(ctr: dagger.Container):
for key, value in envs:
ctr = ctr.with_env_variable(key, value)
return ctr
return env_variables_inner
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Set environment variables in a container
*/
@func()
async setEnv(): Promise<string> {
return await dag
.container()
.from("alpine")
.with(
envVariables([
["ENV_VAR_1", "VALUE 1"],
["ENV_VAR_2", "VALUE 2"],
["ENV_VAR_3", "VALUE_3"],
]),
)
.withExec(["env"])
.stdout()
}
}
function envVariables(envs: Array<[string, string]>) {
return (c: Container): Container => {
for (const [key, value] of envs) {
c = c.withEnvVariable(key, value)
}
return c
}
}
Example
Set environment variables in a container:
dagger call set-env
Persist service state across runs
The following Dagger Function uses a cache volume to persist a Redis service's data across Dagger Function runs.
- Go
- Python
- TypeScript
package main
import (
"context"
"main/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()
redisCLI := dag.Container().
From("redis").
WithServiceBinding("redis-srv", redisSrv).
WithEntrypoint([]string{"redis-cli", "-h", "redis-srv"})
return redisCLI
}
// 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}).
WithExec([]string{"save"}).
Stdout(ctx)
}
// Get value from Redis service
func (m *MyModule) Get(
ctx context.Context,
// The cache value to set
key string,
) (string, error) {
return m.Redis(ctx).
WithExec([]string{"get", key}).
Stdout(ctx)
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
def redis(self) -> dagger.Container:
"""Create Redis service and client"""
redis_srv = (
dag.container()
.from_("redis")
.with_exposed_port(6379)
.with_mounted_cache("/data", dag.cache_volume("my-redis"))
.with_workdir("/data")
.as_service()
)
# create Redis client container
redis_cli = (
dag.container()
.from_("redis")
.with_service_binding("redis-srv", redis_srv)
.with_entrypoint(["redis-cli", "-h", "redis-srv"])
)
return redis_cli
@function
async def set(
self,
key: Annotated[str, Doc("The cache key to set")],
value: Annotated[str, Doc("The cache value to set")],
) -> str:
"""Set key and value in Redis service"""
return (
await self.redis()
.with_exec(["set", key, value])
.with_exec(["save"])
.stdout()
)
@function
async def get(
self,
key: Annotated[str, Doc("The cache key to get")],
) -> str:
"""Get value from Redis service"""
return await self.redis().with_exec(["get", key]).stdout()
import { dag, object, func, Container } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Create Redis service and client
*/
@func()
redis(): Container {
const redisSrv = dag
.container()
.from("redis")
.withExposedPort(6379)
.withMountedCache("/data", dag.cacheVolume("my-redis"))
.withWorkdir("/data")
.asService()
const redisCLI = dag
.container()
.from("redis")
.withServiceBinding("redis-srv", redisSrv)
.withEntrypoint(["redis-cli", "-h", "redis-srv"])
return redisCLI
}
/**
* Set key and value in Redis service
*/
@func()
async set(
/**
* The cache key to set
*/
key: string,
/**
* The cache value to set
*/
value: string,
): Promise<string> {
return await this.redis()
.withExec(["set", key, value])
.withExec(["save"])
.stdout()
}
/**
* Get value from Redis service
*/
@func()
async get(
/**
* The cache key to get
*/
key: string,
): Promise<string> {
// set and save value
return await this.redis().withExec(["get", key]).stdout()
}
}
Example
Save data to a Redis service which uses a cache volume to persist a key named foo
with value `123:
dagger call set --key=foo --value=123
Retrieve the value of the key foo
after recreating the service state from the cache volume:
# returns "123"
dagger call get --key=foo
Error handling
Terminate gracefully
The following Dagger Function demonstrates how to handle errors in a pipeline.
- Go
- Python
- TypeScript
package main
import (
"context"
"errors"
"fmt"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Generate an error
func (m *MyModule) Test(ctx context.Context) (string, error) {
out, err := dag.
Container().
From("alpine").
// ERROR: cat: read error: Is a directory
WithExec([]string{"cat", "/"}).
Stdout(ctx)
var e *dagger.ExecError
if errors.As(err, &e) {
return fmt.Sprintf("Test pipeline failure: %s", e.Stderr), nil
} else if err != nil {
return "", err
}
return out, nil
}
from dagger import DaggerError, dag, function, object_type
@object_type
class MyModule:
@function
async def test(self) -> str:
"""Generate an error"""
try:
return (
await (
dag.container()
.from_("alpine")
# ERROR: cat: read error: Is a directory
.with_exec(["cat", "/"])
.stdout()
)
)
except DaggerError as e:
# DaggerError is the base class for all errors raised by dagger
return "Test pipeline failure: " + e.stderr
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Generate an error
*/
@func()
async test(): Promise<string> {
try {
return await dag
.container()
.from("alpine")
// ERROR: cat: read error: Is a directory
.withExec(["cat", "/"])
.stdout()
} catch (e) {
return `Test pipeline failure: ${e.stderr}`
}
}
}
Example
Execute a Dagger Function which creates a container and runs a command in it. If the command fails, the error is captured and the Dagger Function is gracefully terminated with a custom error message.
dagger call test
Continue using a container after command execution fails
The following Dagger Function 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.
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.
- Go
- Python
- TypeScript
package main
import (
"context"
"fmt"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
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
`
type TestResult struct {
Report *dagger.File
ExitCode string
}
// Handle errors
func (m *MyModule) Test(ctx context.Context) (*TestResult, error) {
ctr, err := dag.
Container().
From("alpine").
// add script with execution permission to simulate a testing tool
WithNewFile("run-tests", script, dagger.ContainerWithNewFileOpts{Permissions: 0o750}).
// if the exit code isn't needed: "run-tests; true"
WithExec([]string{"sh", "-c", "/run-tests; echo -n $? > /exit_code"}).
// the result of `sync` is the container, which allows continued chaining
Sync(ctx)
if err != nil {
// unexpected error, could be network failure.
return nil, fmt.Errorf("run tests: %w", err)
}
// save report for inspection.
report := ctr.File("report.txt")
// use the saved exit code to determine if the tests passed.
exitCode, err := ctr.File("/exit_code").Contents(ctx)
if err != nil {
// exit code not found
return nil, fmt.Errorf("get exit code: %w", err)
}
// Return custom type
return &TestResult{
Report: report,
ExitCode: exitCode,
}, nil
}
import dagger
from dagger import DaggerError, dag, field, function, object_type
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
"""
@object_type
class TestResult:
report: dagger.File = field()
exit_code: str = field()
@object_type
class MyModule:
@function
async def test(self) -> TestResult:
"""Handle errors"""
try:
ctr = (
await (
dag.container().from_("alpine")
# add script with execution permission to simulate a testing tool.
.with_new_file("run-tests", SCRIPT, permissions=0o750)
# if the exit code isn't needed: "run-tests; true"
.with_exec(["sh", "-c", "/run-tests; echo -n $? > /exit_code"])
# the result of `sync` is the container, which allows continued chaining
.sync()
)
)
# save report for inspection.
report = ctr.file("report.txt")
# use the saved exit code to determine if the tests passed.
exit_code = await ctr.file("exit_code").contents()
return TestResult(report=report, exit_code=exit_code)
except DaggerError as e:
# DaggerError is the base class for all errors raised by Dagger
msg = "Unexpected Dagger error"
raise RuntimeError(msg) from e
# ruff: noqa: RET505
import { dag, object, field, func, File } from "@dagger.io/dagger"
const 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
`
@object()
class TestResult {
@field()
report: File
@field()
exitCode: string
}
@object()
class MyModule {
/**
* Handle errors
*/
@func()
async test(): Promise<TestResult> {
try {
const ctr = await dag
.container()
.from("alpine")
// add script with execution permission to simulate a testing tool.
.withNewFile("run-tests", SCRIPT, { permissions: 0o750 })
// if the exit code isn't needed: "run-tests; true
.withExec(["sh", "-c", "/run-tests; echo -n $? > /exit_code"])
// the result of `sync` is the container, which allows continued chaining
.sync()
const result = new TestResult()
// save report for inspection.
result.report = ctr.file("report.txt")
// use the saved exit code to determine if the tests passed
result.exitCode = await ctr.file("exit_code").contents()
return result
} catch (e) {
console.error(e)
}
}
}
Example
Continue executing a Dagger Function even after a command within it fails. The Dagger Function returns a custom TestResult
object containing a test report and the exit code of the failed command.
# get exit code
dagger call test exit-code
# get test report
dagger call test report contents