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.
When working with private Git repositories, ensure that SSH authentication is properly configured on your Dagger host.
- 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 public
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 private
user/foo
GitHub repository to/src
in the container and return the modified container:dagger call copy-directory --source=ssh://git@github.com/user/foo#main
-
Copy the public
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.
When working with private Git repositories, ensure that SSH authentication is properly configured on your Dagger host.
- 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 public
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 private
user/foo
GitHub repository to/src
in the container, add a file to it, and return the modified container:dagger call copy-and-modify-directory --source=ssh://git@github.com/user/foo#main
-
Copy the public
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.
When working with private Git repositories, ensure that SSH authentication is properly configured on your Dagger host.
- 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 public
dagger/dagger
GitHub repository to/src
in the container, excluding all Markdown 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
-
Copy the private
user/foo
GitHub repository to/src
in the container, excluding all Markdown files, and list the contents of the directory:dagger call copy-directory-with-exclusions --source=ssh://git@github.com/user/foo#main --exclude-file='*.md' directory --path=/src entries
Copy a file to the Dagger module runtime container for custom processing
The following Dagger Function accepts a File
argument and copies the specified file to the Dagger module runtime container. This makes it possible to add one or more files to the runtime container and manipulate them using custom logic.
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
"fmt"
"os"
)
type MyModule struct{}
// Copy a file to the Dagger module runtime container for custom processing
func (m *MyModule) CopyFile(ctx context.Context, source *dagger.File) {
source.Export(ctx, "foo.txt")
// your custom logic here
// for example, read and print the file in the Dagger Engine container
fmt.Println(os.ReadFile("foo.txt"))
}
import anyio
import dagger
from dagger import function, object_type
@object_type
class MyModule:
@function
async def copy_file(self, source: dagger.File):
"""Copy a file to the Dagger module runtime container for custom processing"""
await source.export("foo.txt")
# your custom logic here
# for example, read and print the file in the Dagger Engine container
print(await anyio.Path("foo.txt").read_text())
import { object, func, File } from "@dagger.io/dagger"
import * as fs from "fs"
@object()
class MyModule {
// Copy a file to the Dagger module runtime container for custom processing
@func()
async copyFile(source: File) {
await source.export("foo.txt")
// your custom logic here
// for example, read and print the file in the Dagger Engine container
console.log(fs.readFileSync("foo.txt", "utf8"))
}
}
Example
Copy the data.json
host file to the runtime container and process it:
dagger call copy-file --source=../data.json
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 layer 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 layer 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
Print the date and time, invalidating the cache on each run. However, if the CACHEBUSTER
environment variable is removed, the same value (the date and time on the first run) is printed on every run.
dagger 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 "Authorization: Bearer $GITHUB_API_TOKEN"'
' --header "Accept: application/vnd.github+json"'
),
]
)
.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"
"main/internal/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) {
// Ensure the Dagger secret's name matches what the Dockerfile
// expects as the id for the secret mount.
secretVal, err := secret.Plaintext(ctx)
if err != nil {
return nil, err
}
buildSecret := dag.SetSecret("gh-secret", secretVal)
return source.
DockerBuild(dagger.DirectoryDockerBuildOpts{
Secrets: []*dagger.Secret{buildSecret},
}), nil
}
from typing import Annotated
import dagger
from dagger import Doc, dag, 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"""
# Ensure the Dagger secret's name matches what the Dockerfile
# expects as the id for the secret mount.
build_secret = dag.set_secret("gh-secret", await secret.plaintext())
return source.docker_build(secrets=[build_secret])
import { dag, 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> {
// Ensure the Dagger secret's name matches what the Dockerfile
// expects as the id for the secret mount.
const buildSecret = dag.setSecret("gh-secret", await secret.plaintext())
return source.dockerBuild({ secrets: [buildSecret] })
}
}
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)
args := []string{"redis-cli", "-h", "redis-srv"}
// set value
setter, err := redisCLI.
WithExec(append(args, "set", "foo", "abc")).
Stdout(ctx)
if err != nil {
return "", err
}
// get value
getter, err := redisCLI.
WithExec(append(args, "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)
)
args = ["redis-cli", "-h", "redis-srv"]
# set value
setter = await redis_cli.with_exec([*args, "set", "foo", "abc"]).stdout()
# get value
getter = await redis_cli.with_exec([*args, "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)
const args = ["redis-cli", "-h", "redis-srv"]
// set value
const setter = await redisCLI
.withExec([...args, "set", "foo", "abc"])
.stdout()
// get value
const getter = await redisCLI.withExec([...args, "get", "foo"]).stdout()
return setter + getter
}
}
Example
Start and stop a Redis service:
dagger call redis-service
Create interdependent services
The following Dagger Function runs two services, service A and service B, that depend on each other. The services are set up with custom hostnames, svca
and svcb
, allowing each service to communicate with the other by hostname.
- Go
- Python
- TypeScript
package main
import (
"context"
"main/internal/dagger"
)
type MyModule struct{}
// Run two services which are dependent on each other
func (m *MyModule) Services(ctx context.Context) (*dagger.Service, error) {
svcA := dag.Container().From("nginx").
WithExposedPort(80).
WithExec([]string{"sh", "-c", `
nginx & while true; do curl svcb:80 && sleep 1; done
`}).
AsService().WithHostname("svca")
_, err := svcA.Start(ctx)
if err != nil {
return nil, err
}
svcB := dag.Container().From("nginx").
WithExposedPort(80).
WithExec([]string{"sh", "-c", `
nginx & while true; do curl svca:80 && sleep 1; done
`}).
AsService().WithHostname("svcb")
svcB, err = svcB.Start(ctx)
if err != nil {
return nil, err
}
return svcB, nil
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def services(self) -> dagger.Service:
"""Run two services which are dependent on each other"""
svc_a = (
dag.container()
.from_("nginx")
.with_exposed_port(80)
.with_exec(
["sh", "-c", "nginx & while true; do curl svcb:80 && sleep 1; done"]
)
.as_service()
.with_hostname("svca")
)
await svc_a.start()
svc_b = (
dag.container()
.from_("nginx")
.with_exposed_port(80)
.with_exec(
["sh", "-c", "nginx & while true; do curl svca:80 && sleep 1; done"]
)
.as_service()
.with_hostname("svcb")
)
await svc_b.start()
return svc_b
import { object, func, Service } from "@dagger.io/dagger"
@object()
class MyModule {
// Run two services which are dependent on each other
@func()
async services(): Promise<Service> {
const svcA = dag
.container()
.from("nginx")
.withExposedPort(80)
.withExec([
"sh",
"-c",
`nginx & while true; do curl svcb:80 && sleep 1; done`,
])
.asService()
.withHostname("svca")
await svcA.start()
const svcB = dag
.container()
.from("nginx")
.withExposedPort(80)
.withExec([
"sh",
"-c",
`nginx & while true; do curl svca:80 && sleep 1; done`,
])
.asService()
.withHostname("svcb")
await svcB.start()
return svcB
}
}
Example
Start two inter-dependent services:
dagger call services up --ports 8080:80
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