Quickstart
Write your first function
So far, you've been calling individual Dagger Functions using the Dagger CLI. But what if you want to combine multiple Dagger Function calls?
One option is to stitch together calls using the CLI and a shell script or a Makefile However, this approach is not recommended, because you will typically end up with long and unwieldy CLI commands and shell scripting "glue" that is hard to debug and maintain.
A better approach is to create one or more Dagger Function(s), which can then call other Dagger Functions to achieve the required results. Creating your own Dagger Function(s) enables you to transform your workflows into structured, discrete and composable units with clear inputs and outputs, with all the benefits of using a native programming language.
To see this in action, create a custom Dagger Function that uses two other Dagger modules to build your project, add it to a custom container image of your choice, and publish the result to a registry.
The steps below assume that you have already Daggerized the example Go project repository by initializing a new Dagger module and installing the Go builder module as a dependency. If not, complete those steps before proceeding.
Bootstrap a module template
Begin by running dagger develop
to bootstrap a module template in one of the supported programming languages:
- Go
- Python
- TypeScript
dagger develop --sdk=go
This will generate a dagger.json
module file, an initial dagger/main.go
source file, as well as a generated dagger/dagger.gen.go
and dagger/internal
directory for the generated module code.
dagger develop --sdk=python
This will generate a dagger.json
module file, initial dagger/src/main/__init__.py
, dagger/pyproject.toml
and dagger/requirements.lock
files, as well as a generated dagger/sdk
folder for local development.
dagger develop --sdk=typescript
This will generate a dagger.json
module file, initial dagger/src/index.ts
, dagger/package.json
and dagger/tsconfig.json
files, as well as a generated dagger/sdk
folder for local development.
The class/type definitions in the generated module code are derived from the module name.
Install another dependency
You've already installed the Go builder module in your project. Next, install the Wolfi container builder module from earlier as another dependency:
dagger install github.com/shykes/daggerverse/wolfi@v0.1.2
Add a function to the module
Update the generated module template:
- Go
- Python
- TypeScript
Update dagger/main.go
with the following code:
package main
import (
"context"
"fmt"
"math"
"math/rand"
)
type Example struct{}
// Build and publish a project using a Wolfi container
func (m *Example) BuildAndPublish(
ctx context.Context,
buildSrc *Directory,
buildArgs []string,
) (string, error) {
ctr := dag.Wolfi().Container()
return dag.
Golang().
BuildContainer(
GolangBuildContainerOpts{
Source: buildSrc,
Args: buildArgs,
Base: ctr,
}).
Publish(
ctx,
fmt.Sprintf(
"ttl.sh/my-hello-container-%.0f",
math.Floor(rand.Float64()*10000000),
),
) //#nosec
}
This new BuildAndPublish()
function accepts two arguments - the source directory and a list of build arguments - and does the following:
- It invokes the
Wolfi
module via thedag
client. - It calls the module's
Container()
function to return a new Wolfi container image as aContainer
. - It invokes the
Golang
module via thedag
Dagger client. - It calls the module's
BuildContainer()
function to build the source code and return aContainer
with the compiled binary. - It calls the
Container.Publish()
method to publish the container image to a registry and print the SHA identifier of the published image.
Update dagger/src/main/__init__.py
with the following code:
import uuid
import dagger
from dagger import dag, function, object_type
@object_type
class Example:
@function
async def build_and_publish(
self, build_src: dagger.Directory, build_args: list[str]
) -> str:
"""Build and publish a project using a Wolfi container"""
# retrieve a new Wolfi container
ctr = dag.wolfi().container()
# publish the Wolfi container with the build result
return await (
dag.golang()
.build_container(source=build_src, args=build_args, base=ctr)
.publish(f"ttl.sh/my-hello-container-{uuid.uuid4().hex[:8]}")
)
This new build_and_publish()
function accepts two arguments - the source directory and a list of build arguments - and does the following:
- It invokes the
Wolfi
module via thedag
client. - It calls the module's
container()
function to return a new Wolfi container image as aContainer
. - It invokes the
Golang
module via thedag
Dagger client. - It calls the module's
build_container()
function to build the source code and return aContainer
with the compiled binary. - It calls the
Container.publish()
method to publish the container image to a registry and print the SHA identifier of the published image.
Update dagger/src/index.ts
with the following code:
import { dag, Directory, object, func } from "@dagger.io/dagger"
@object()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class Example {
@func()
async buildAndPublish(
buildSrc: Directory,
buildArgs: string[],
): Promise<string> {
const ctr = dag.wolfi().container()
return await dag
.golang()
.buildContainer({ source: buildSrc, args: buildArgs, base: ctr })
.publish("ttl.sh/my-api-server-" + Math.floor(Math.random() * 10000000))
}
}
This new buildAndPublish()
function accepts two arguments - the source directory and a list of build arguments - and does the following:
- It invokes the
Wolfi
module via thedag
client. - It calls the module's
container()
function to return a new Wolfi container image as aContainer
. - It invokes the
Golang
module via thedag
Dagger client. - It calls the module's
buildContainer()
function to build the source code and return aContainer
with the compiled binary. - It calls the
Container.publish()
method to publish the container image to a registry and print the SHA identifier of the published image.
Call the function
Call the new function using the Dagger CLI:
dagger call build-and-publish \
--build-src=. \
--build-args="-C","./hello"
Here's an example of the output you should see:
ttl.sh/my-hello-container-3056246@sha256:ef0ffb71286be8c51e3d432fb9428ce46c80a32e8456dc963d01798fcba1e880
Test the published container image using the command below (remember to update the image name based on the function output):
docker run \
--name ctr \
--rm \
-it ttl.sh/my-hello-container-3056246 /usr/local/bin/hello
You should see the same output as before:
Hello, world!
Well done! You've just successfully written your first Dagger Function!
When you install one module in another, Dagger exposes its functions using a language-agnostic GraphQL API. This means that any function can call any other function, regardless of whether they use the same programming language or not. For example, a Go function can call a Python function, which can call a Typescript function, and so on. This also means that you no longer care which language your CI tooling is written in; you can use the one that you're most comfortable with or that best suits your requirements. Learn more about how Dagger works.