Quickstart
Welcome to Dagger, a general-purpose composition engine for containerized workflows.
Dagger is a modular, composable platform designed to replace complex systems glued together with artisanal scripts - for example, complex integration testing environments, data processing pipelines, and AI agent workflows. It is open source and works with any compute platform or technology stack, automatically optimizing for speed and cost.
Create containers
Dagger works by expressing workflows as combinations of functions from the Dagger API, which exposes a range of tools for working with containers, files, directories, network services, secrets. You call the Dagger API from the shell (Dagger Shell) or from code (custom Dagger Functions written in a programming language).
Dagger Shell is an interactive client for the Dagger API, giving you typed objects, built-in documentation, and access to a cross-language ecosystem of reusable modules. You launch it by typing dagger
. Here's an example of using it to build and return an Alpine container:
container | from alpine
You can also run commands in the container and return the output. Here's an example of returning the output of the uname
command:
container | from alpine | with-exec uname | stdout
Here's an example of installing the curl
package in the container and using it to retrieve a webpage:
container | from alpine | with-exec apk add curl | with-exec curl https://dagger.io | stdout
Add files and directories
Once you've got a container, you continue using the Dagger API to modify it, by adding files or directories to it. You can use directories from the Dagger host's filesystem or remote Git repositories. Here's an example of adding Dagger's own open source GitHub repository to the container:
container | from alpine | with-directory /src https://github.com/dagger/dagger
Here's another example, this time creating a new file in the container:
container | from alpine | with-new-file /hi.txt "Hello from Dagger!"
The Dagger API is extensively documented but if you're unsure how to proceed at any point, simply append .help
to your in-progress workflow for context-sensitive assistance on what you can do next. For example:
container | from alpine | .help
container | from alpine | .help with-directory
container | from alpine | .help with-exec
Use the interactive terminal
Did it work? To look inside, request an interactive terminal session with the running container:
container | from alpine | with-new-file /hi.txt "Hello from Dagger!" | terminal
This drops you into an interactive terminal running the bash
shell. You now directly execute commands in the running container, as shown below:
The terminal
function is very useful for debugging and experimenting, since it allows you to interact directly with containers and inspect their state, at any stage of your Dagger Function execution.
Chain functions in the shell
What you just did (and have been doing since the start) is chaining one Dagger API function call to another with the pipe (|
) operator. This is one of Dagger's most powerful features, as it allows you create dynamic workflows in a single command - no context switching between Dockerfile creation, build commands, and registry pushes.
Dagger's documentation has numerous examples of function chaining in action but here's one more: creating an Alpine container, dropping in a text file with a custom message, setting it to display that message when run, and publishing it to a temporary registry - all in a single command!
container | from alpine | with-new-file /hi.txt "Hello from Dagger!" |
with-entrypoint cat /hi.txt | publish ttl.sh/hello
Write custom functions
As your workflows become more complex, you'll start wishing you could make them more reusable, repeatable and shareable. To do this, encapsulate your workflows into custom Dagger Functions. These are just regular code consisting of a series of method/function calls, such as "pull a container image", "copy a file", "forward a TCP port", and so on, which can be chained together. They are written in a programming language using a type-safe Dagger SDK and packaged into modules.
Here's the previous example, rewritten as a Dagger Function:
- Go
- Python
- TypeScript
- PHP
- Java
func (m *Basics) Publish(ctx context.Context) (string, error) {
return dag.Container().
From("alpine:latest").
WithNewFile("/hi.txt", "Hello from Dagger!").
WithEntrypoint([]string{"cat", "/hi.txt"}).
Publish(ctx, "ttl.sh/hello")
}
@function
async def publish(self) -> str:
return await (
dag.container()
.from_("alpine:latest")
.with_new_file("/hi.txt", "Hello from Dagger!")
.with_entrypoint(["cat", "/hi.txt"])
.publish("ttl.sh/hello")
)
@func()
async publish(): Promise<string> {
return dag
.container()
.from("alpine:latest")
.withNewFile("/hi.txt", "Hello from Dagger!")
.withEntrypoint(["cat", "/hi.txt"])
.publish("ttl.sh/hello")
}
#[DaggerFunction]
public function publish(): string
{
return dag()
->container()
->from('alpine:latest')
->withNewFile('/hi.txt', 'Hello from Dagger!')
->withEntrypoint(['cat', '/hi.txt'])
->publish('ttl.sh/hello');
}
@Function
public String publish()
throws InterruptedException, ExecutionException, DaggerQueryException {
return dag()
.container()
.from("alpine:latest")
.withNewFile("/hi.txt", "Hello from Dagger!")
.withEntrypoint(List.of("cat", "/hi.txt"))
.publish("ttl.sh/hello");
}
When you create a custom Dagger Function and tell Dagger about it (by installing the corresponding module), the Dagger API is dynamically extended to include that new function. You then call this function from Dagger Shell, or from the command-line using dagger call
, in exactly the same way as you would call functions from the original Dagger API.
Chain functions in code
Function chaining works the same way, whether you're writing Dagger Function code using a Dagger SDK or using Dagger Shell. The following are equivalent:
- Go
- Python
- TypeScript
- PHP
- Java
- System shell
- Dagger Shell
// Returns a base container
func (m *Basics) Base() *dagger.Container {
return dag.Container().From("cgr.dev/chainguard/wolfi-base")
}
// Builds on top of base container and returns a new container
func (m *Basics) Build() *dagger.Container {
return m.Base().WithExec([]string{"apk", "add", "bash", "git"})
}
// Builds and publishes a container
func (m *Basics) BuildAndPublish(ctx context.Context) (string, error) {
return m.Build().Publish(ctx, "ttl.sh/bar")
}
@object_type
class Basics:
@function
def base(self) -> dagger.Container:
"""Returns a base container"""
return dag.container().from_("cgr.dev/chainguard/wolfi-base")
@function
def build(self) -> dagger.Container:
"""Builds on top of base container and returns a new container"""
return self.base().with_exec(["apk", "add", "bash", "git"])
@function
async def build_and_publish(self) -> str:
"""Builds and publishes a container"""
return await self.build().publish("ttl.sh/bar")
@object()
class Basics {
/**
* Returns a base container
*/
@func()
base(): Container {
return dag.container().from("cgr.dev/chainguard/wolfi-base")
}
/**
* Builds on top of base container and returns a new container
*/
@func()
build(): Container {
return this.base().withExec(["apk", "add", "bash", "git"])
}
/**
* Builds and publishes a container
*/
@func()
async buildAndPublish(): Promise<string> {
return await this.build().publish("ttl.sh/bar")
}
}
#[DaggerObject]
class Basics
{
#[DaggerFunction]
#[Doc('Returns a base container')]
public function base(): Container
{
return dag()
->container()
->from('cgr.dev/chainguard/wolfi-base');
}
#[DaggerFunction]
#[Doc('Builds on top of base container and returns a new container')]
public function build(): Container
{
return $this
->base()
->withExec(['apk', 'add', 'bash', 'git']);
}
#[DaggerFunction]
#[Doc('Builds and publishes a container')]
public function buildAndPublish(): string
{
return $this
->build()
->publish('ttl.sh/bar');
}
}
@Object
public class Basics {
/**
* Returns a base container
*/
@Function
public Container base() {
return dag().container().from("cgr.dev/chainguard/wolfi-base");
}
/**
* Builds on top of base container and returns a new container
*/
@Function
public Container build() {
return this.base().withExec(List.of("apk", "add", "bash", "git"));
}
/**
* Builds and publishes a container
*/
@Function
public String buildAndPublish()
throws InterruptedException, ExecutionException, DaggerQueryException {
return this.build().publish("ttl.sh/bar");
}
}
# all equivalent
dagger -c 'base | with-exec apk add bash git | publish ttl.sh/bar'
dagger -c 'build | publish ttl.sh/bar'
dagger -c build-and-publish
# all equivalent
base | with-exec apk add bash git | publish ttl.sh/bar
build | publish ttl.sh/bar
build-and-publish
When calling Dagger Functions, all names (functions, arguments, fields, etc.) are converted into a shell-friendly "kebab-case" style. This is why a Dagger Function named FooBar
in Go, foo_bar
in Python and fooBar
in TypeScript/PHP/Java is called as foo-bar
in Dagger Shell or on the command-line.
Use arguments and return values
Dagger Functions are just like regular functions: they accept arguments and return values. In addition to common types (string, boolean, integer, arrays...), the Dagger API also defines powerful core types which Dagger Functions can use, such as Directory
, Container
, Service
, Secret
, and many more.
Here's a revision of the previous example which splits it into two smaller functions: one to build the container, and one to publish it. The builder function accepts the container image string as an argument and returns a Container
type. The publisher function accepts a Container
type as argument and returns the image identifier as a string.
- Go
- Python
- TypeScript
- PHP
- Java
func (m *Basics) Build(
// +default "alpine:latest"
image string,
) *dagger.Container {
return dag.Container().
From(image).
WithNewFile("/hi.txt", "Hello from Dagger!")
}
func (m *Basics) Publish(
ctx context.Context,
// +default "alpine:latest"
image string,
) (string, error) {
return m.Build(image).
WithEntrypoint([]string{"cat", "/hi.txt"}).
Publish(ctx, "ttl.sh/hello")
}
@function
def build(self, image: str = "alpine:latest") -> dagger.Container:
return (
dag.container().from_(image).with_new_file("/hi.txt", "Hello from Dagger!")
)
@function
async def publish(self, image: str = "alpine:latest") -> str:
return await (
self.build(image)
.with_entrypoint(["cat", "/hi.txt"])
.publish("ttl.sh/hello")
)
@func()
build(image = "alpine:latest"): Container {
return dag
.container()
.from(image)
.withNewFile("/hi.txt", "Hello from Dagger!")
}
@func()
async publish(image = "alpine:latest"): Promise<string> {
return this
.build(image)
.withEntrypoint(["cat", "/hi.txt"])
.publish("ttl.sh/hello")
}
#[DaggerFunction]
public function build(string $image = 'alpine:latest'): Container
{
return dag()
->container()
->from($image)
->withNewFile('/hi.txt', 'Hello from Dagger!');
}
#[DaggerFunction]
public function publish2(string $image = 'alpine:latest'): string
{
return $this
->build($image)
->withEntrypoint(['cat', '/hi.txt'])
->publish('ttl.sh/hello');
}
@Function
public Container build(@Default("alpine:latest") String image) {
return dag()
.container()
.from("alpine:latest")
.withNewFile("/hi.txt", "Hello from Dagger!");
}
@Function
public String publish2(@Default("alpine:latest") String image)
throws InterruptedException, ExecutionException, DaggerQueryException {
return this
.build(image)
.withEntrypoint(List.of("cat", "/hi.txt"))
.publish("ttl.sh/hello");
}
Dagger Functions are fully "sandboxed" and do not have direct access to the host system. Therefore, host resources such as directories, files, environment variables, network services and so on must be explicitly passed to Dagger Functions as arguments. This "sandboxing" of Dagger Functions improves security, ensures reproducibility, and assists caching.
Install other modules
You can group Dagger Functions into modules and share them with others - your team, your company, or the broader Dagger community. And just as others can use your modules, you too can use modules created and shared by others, to speed up your development and take advantage of best practices. The Daggerverse is a free service run by Dagger, which indexes all publicly available Dagger modules, and lets you easily search and consume them.
Here's an example of installing and using two modules from the Daggerverse: a Wolfi container builder and a Trivy container scanner:
Here's what it looks like in code:
- Go
- Python
- TypeScript
- PHP
- Java
func (m *Basics) Check(ctx context.Context) (string, error) {
ctr := dag.Wolfi().Container()
return dag.Trivy().
ScanContainer(ctx, ctr);
}
@function
def check(self) -> str:
ctr = dag.wolfi().container()
return dag.trivy().scan_container(ctr)
@func()
check(): string {
let ctr = dag.wolfi().container()
return dag.trivy().scanContainer(ctr);
}
#[DaggerFunction]
public function check(): string
{
$ctr = dag()
->wolfi()
->container();
return dag()
->trivy()
->scanContainer($ctr);
}
@Function
public String check()
throws InterruptedException, ExecutionException, DaggerQueryException {
Container ctr = dag()
.wolfi()
.container();
return dag()
.trivy()
.scanContainer(ctr);
}
Dagger Functions can call other Dagger Functions, across languages. For example, a Python function can call a Go function, which can call a TypeScript function, and so on. This means that you no longer need to care which language your workflow is written in; you can use the one that you're most comfortable with or that best suits your requirements.
Speed things up
One of Dagger's most powerful features is its ability to cache data across workflow runs. Dagger caches two types of data:
- Layers: Build instructions and the results of some API calls.
- Volumes: Contents of Dagger filesystem volumes.
Taken together, these two types of caching significantly reduce execution times. Here's an example: a Dagger Function that creates a cache volume to store the packages installed by apt
:
- Go
- Python
- TypeScript
- PHP
- Java
func (m *Basics) Env(ctx context.Context) *dagger.Container {
aptCache := dag.CacheVolume("apt-cache")
return dag.Container().
From("debian:latest").
WithMountedCache("/var/cache/apt/archives", aptCache).
WithExec([]string{"apt-get", "update"}).
WithExec([]string{"apt-get", "install", "--yes", "maven", "mariadb-server"})
}
@function
def env(self) -> dagger.Container:
apt_cache = dag.cache_volume("apt-cache")
return (
dag.container()
.from_("debian:latest")
.with_mounted_cache("/var/cache/apt/archives", apt_cache)
.with_exec(["apt-get", "update"])
.with_exec(["apt-get", "install", "--yes", "maven", "mariadb-server"])
)
@func()
env(): Container {
let aptCache = dag.cacheVolume("apt-cache")
return dag.container()
.from("debian:latest")
.withMountedCache("/var/cache/apt/archives", aptCache)
.withExec(["apt-get", "update"])
.withExec(["apt-get", "install", "--yes", "maven", "mariadb-server"])
}
#[DaggerFunction]
public function env(): Container
{
$aptCache = dag()->cacheVolume('apt-cache');
return dag()
->container()
->from('debian:latest')
->withMountedCache('/var/cache/apt/archives', $aptCache)
->withExec(['apt-get', 'update'])
->withExec(['apt-get', 'install', '--yes', 'maven', 'mariadb-server']);
}
@Function
public Container env() {
CacheVolume aptCache = dag().cacheVolume("apt-cache");
return dag()
.container()
.from("debian:latest")
.withMountedCache("/var/cache/apt/archives", aptCache)
.withExec(List.of("apt-get", "update"))
.withExec(List.of("apt-get", "install", "--yes", "maven", "mariadb-server"));
}
Notice that when you call this Dagger Function multiple times, the second and subsequent runs are drastically faster than the first, since Dagger automatically reuses cached instructions and files from the cache.
Trace everything
Building and running workflows is only part of the problem - you also need a way to inspect and monitor them. Dagger provides two powerful real-time visualization tools: the Dagger terminal UI (TUI), which you've already seen above, and Dagger Cloud, a browser-based interface focused on tracing and debugging Dagger workflows.
Once configured, every time you execute a Dagger workflow, its operational telemetry is automatically sent to Dagger Cloud as a Trace and the workflow output includes a link to visualize the workflow run on Dagger Cloud. Here's an example:
Dagger Cloud sign-up is optional, and free of charge for a single user.
Next steps
Now that you know the basics of Dagger, continue your journey with the resources below: