Custom Functions
In addition to providing a set of core functions and types, the Dagger API can also be extended with custom Dagger Functions and custom types. These custom Dagger Functions are just regular code, written in your usual language using a type-safe Dagger SDK, and packaged and shared in Dagger modules.
When a Dagger module is loaded into a Dagger session, the Dagger API is dynamically extended with new functions served by that module. So, after loading a Dagger module, an API client can now call all of the original core functions plus the new functions provided by that module.
Initialize a Dagger module​
Dagger Functions are packaged, shared and reused using Dagger modules. A new Dagger module is initialized by calling dagger init. This creates a new dagger.json configuration file in the current working directory, together with sample Dagger Function source code. The configuration file will default the name of the module to the current directory name, unless an alternative is specified with the --name argument.
Once a module is initialized, dagger develop --sdk=... sets up or updates all the resources needed to develop the module locally. By default, the module source code will be stored in the current working directory, unless an alternative is specified with the --source argument.
The default template from dagger develop creates the following structure:
- Go
- Python
- TypeScript
- PHP
- Java
.
├── LICENSE
├── dagger.gen.go
├── go.mod
├── go.sum
├── internal
│ ├── dagger
│ ├── querybuilder
│ └── telemetry
└── main.go
└── dagger.json
In this structure:
dagger.jsonis the Dagger module configuration file.go.mod/go.summanage the Go module and its dependencies.main.gois where your Dagger module code goes. It contains sample code to help you get started.internalcontains automatically-generated types and helpers needed to configure and run the module:daggercontains definitions for the Dagger API that's tied to the currently running Dagger Engine container.querybuilderhas utilities for building GraphQL queries (used internally by thedaggerpackage).telemetryhas utilities for sending Dagger Engine telemetry.
For examples of modules written in Go, see Daggerverse Modules in Go.
.
├── LICENSE
├── pyproject.toml
├── uv.lock
├── sdk
├── src
│ └── my_module
│ ├── __init__.py
│ └── main.py
└── dagger.json
In this structure:
dagger.jsonis the Dagger module configuration file.pyproject.tomlmanages the Python project configuration.uv.lockmanages the module's pinned dependencies.src/my_module/is where your Dagger module code goes. It contains sample code to help you get started.sdk/contains the vendored Python SDK client library.
Placing the source code under a src directory follows a common Python convention. To know more, see src layout vs flat layout.
For examples of modules written in Python, see Daggerverse Modules in Python.
.
├── LICENSE
├── package.json
├── sdk
├── src
│ └── index.ts
└── tsconfig.json
└── dagger.json
In this structure:
dagger.jsonis the Dagger module configuration file.package.jsonmanages the module dependencies.src/is where your Dagger module code goes. It contains sample code to help you get started.sdk/contains the TypeScript SDK.
For examples of modules written in Typescript, see Daggerverse Modules in Typescript.
.
├── composer.json
├── composer.lock
├── dagger.json
├── LICENSE
├── README.md
├── sdk
├── src
│ └── MyModule.php
└── vendor
In this structure:
dagger.jsonis the Dagger module configuration file.composer.jsonmanages the module dependencies.src/is where your Dagger module code goes. It contains sample code to help you get started.sdk/contains the PHP SDK.
For examples of modules written in PHP, see Daggerverse Modules in PHP.
.
├── dagger.json
├── pom.xml
├── src
│ └── main
│ └── java
│ └── io
│ └── dagger
│ └── modules
│ └── mymodule
│ ├── MyModule.java
│ └── package-info.java
└── target
└── generated-sources
├── dagger-io
├── dagger-module
└── entrypoint
In this structure:
dagger.jsonis the Dagger module configuration file.pom.xmlmanages the module dependencies.src/main/java/io/dagger/modules/mymodule/is where your Dagger module code goes. It contains sample code to help you get started.MyModule.javais the main class that contains the Dagger Functions.package-info.javais the package information file and is the place to document the module.
target/generated-sources/contains the generated Dagger code:dagger-iocontains the Java specific library for Dagger.dagger-modulecontains all the types generated by Dagger and accessible from the module.entrypointcontains the generated entrypoint for the module.
The target folder is re-generated every time you run dagger develop and enables code completion
and type hinting in the IDE.
The pom.xml is configured to automatically set the generated entrypoint as the main class so the generated JAR
can be easily run.
While you can use the utilities defined in the automatically-generated code above, you cannot edit these files. Even if you edit them locally, any changes will not be persisted when you run the module.
Create a Dagger Function​
Here's an example of a Dagger Function which calls a remote API method and returns the result:
- Go
- Python
- TypeScript
- PHP
- Java
Update the main.go file with the following code:
package main
import (
"context"
)
type MyModule struct{}
func (m *MyModule) GetUser(ctx context.Context) (string, error) {
return dag.Container().
From("alpine:latest").
WithExec([]string{"apk", "add", "curl"}).
WithExec([]string{"apk", "add", "jq"}).
WithExec([]string{"sh", "-c", "curl https://randomuser.me/api/ | jq .results[0].name"}).
Stdout(ctx)
}
This Dagger Function includes the context as input and error as return in its signature.
Update the src/my_module/main.py file with the following code:
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def get_user(self) -> str:
return await (
dag.container()
.from_("alpine:latest")
.with_exec(["apk", "add", "curl"])
.with_exec(["apk", "add", "jq"])
.with_exec(
["sh", "-c", "curl https://randomuser.me/api/ | jq .results[0].name"]
)
.stdout()
)
Dagger Functions are implemented as @dagger.function decorated methods, of a @dagger.object_type decorated class.
It's possible for a module to implement multiple classes (object types), but the first one needs to have a name that matches the module's name, in PascalCase. This object is sometimes referred to as the main object.
For example, for a module initialized with dagger init --name=my-module,
the main object needs to be named MyModule.
Update the src/index.ts file with the following code:
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
async getUser(): Promise<string> {
return await dag
.container()
.from("alpine:latest")
.withExec(["apk", "add", "curl"])
.withExec(["apk", "add", "jq"])
.withExec([
"sh",
"-c",
"curl https://randomuser.me/api/ | jq .results[0].name",
])
.stdout()
}
}
Update the src/MyModule.php file with the following code:
<?php
declare(strict_types=1);
namespace DaggerModule;
use Dagger\Attribute\{DaggerObject, DaggerFunction};
use function Dagger\dag;
#[DaggerObject]
class MyModule
{
#[DaggerFunction]
public function getUser(): string
{
return dag()
->container()
->from('alpine:latest')
->withExec(['apk', 'add', 'curl'])
->withExec(['apk', 'add', 'jq'])
->withExec([
'sh',
'-c',
'curl https://randomuser.me/api/ | jq .results[0].name',
])
->stdout();
}
}
Update the src/main/java/io/dagger/modules/mymodule/MyModule.java file with the following code:
package io.dagger.modules.mymodule;
import static io.dagger.client.Dagger.dag;
import io.dagger.client.Container;
import io.dagger.client.Directory;
import io.dagger.client.DaggerQueryException;
import io.dagger.module.annotation.Function;
import io.dagger.module.annotation.Object;
import java.util.List;
import java.util.concurrent.ExecutionException;
/** MyModule main object */
@Object
public class MyModule {
@Function
public String getUser() throws ExecutionException, DaggerQueryException, InterruptedException {
return dag().container()
.from("alpine:latest")
.withExec(List.of("apk", "add", "curl"))
.withExec(List.of("apk", "add", "jq"))
.withExec(List.of("sh", "-c", "curl https://randomuser.me/api/ | jq .results[0].name"))
.stdout();
}
}
Dagger Functions must be public. The function must be decorated with the @Function annotation
and the class containing the functions must be decorated with the @Object annotation.
You can try this Dagger Function by copying it into the default template generated by dagger init, but remember that you must update the module name in the code samples above to match the name used when your module was first initialized.
In simple terms, here is what this Dagger Function does:
- It initializes a new container from an
alpinebase image. - It executes the
apk add ...command in the container to add thecurlandjqutilities. - It uses the
curlutility to send an HTTP request to the URLhttps://randomuser.me/api/and parses the response usingjq. - It retrieves and returns the output stream of the last executed command as a string.
Here is an example call for this Dagger Function:
dagger call get-user
Here's what you should see:
{
"title": "Mrs",
"first": "Beatrice",
"last": "Lavigne"
}
Dagger Functions execute within containers spawned by the Dagger Engine. This "sandboxing" serves a few important purposes:
- Reproducibility: Executing in a well-defined and well-controlled container ensures that a Dagger Function runs the same way every time it is invoked. It also guards against creating "hidden dependencies" on ambient properties of the execution environment that could change at any moment.
- Caching: A reproducible containerized environment makes it possible to cache the result of Dagger Function execution, which in turn allows Dagger to automatically speed up operations.
- Security: Even when running third-party Dagger Functions sourced from a Git repository, those Dagger Functions will not have default access to your host environment (host files, directories, environment variables, etc.). Access to these host resources can only be granted by explicitly passing them as argument values to the Dagger Function.
When implementing Dagger Functions, you are free to write arbitrary code that will execute inside the Dagger module's container. You have access to the Dagger API to make calls to the core Dagger API or other Dagger modules you depend on, but you are also free to just use the language's standard library and/or imported third-party libraries.
The process your code executes in will currently be with the root user, but without a full set of Linux capabilities and other standard container sandboxing provided by runc.
The current working directory of your code will be an initially empty directory. You can write and read files and directories in this directory if needed. This includes using the Container.export(), Directory.export() or File.export() APIs to write those artifacts to this local directory if needed.