Your First Module
Introduction
Welcome to Dagger, a programmable tool that lets you replace your software project's artisanal scripts with a modern API and cross-language scripting engine.
Dagger lets you encapsulate all your project's tasks and workflows into simple Dagger Modules, written in your programming language of choice.
This guide introduces you to Dagger Modules and walks you, step by step, through the process of creating your first Dagger Module.
Requirements
This guide assumes that:
- You have a good understanding of the Dagger TypeScript SDK. If not, refer to the TypeScript SDK reference.
- You have the Dagger CLI installed. If not, install Dagger.
- You have Docker installed and running on the host system. If not, install Docker.
Step 1: Initialize a new Dagger Module
Create a new directory on your filesystem and run dagger init
to bootstrap your first Dagger Module. This guide calls it potato
, but you can choose your favorite food.
# initialize Dagger module
dagger init --sdk=typescript potato
cd potato/
This will generate a dagger.json
module file, initial dagger/src/index.ts
and related files, as well as a generated dagger/sdk
folder for local development.
The auto-generated Dagger Module template comes with some example Dagger Functions. Test it with the dagger call
command:
dagger call container-echo --string-arg='Hello Daggernauts!' stdout
The result will be:
Hello Daggernauts!
When using dagger call
, all names (functions, arguments, struct fields, etc) are converted into a shell-friendly "kebab-case" style.
Step 2: Create a simple Dagger Function
Dagger Modules contain one or more Dagger Functions. You saw one of them in the previous section, but you can also add your own.
Replace the auto-generated examples in dagger/src/index.ts
with your own, simpler Dagger Function:
import { object, func } from "@dagger.io/dagger"
@object()
class Potato {
@func()
helloWorld(): string {
return "Hello Daggernauts!"
}
}
The @object()
decorator exposes the class to the Dagger API and allow calling its methods, which are decorated with @func()
, from the Dagger CLI.
Test the new Dagger Function, once again using dagger call
:
dagger call hello-world
The result will be:
Hello Daggernauts!
Step 3: Add arguments to your Dagger Function
Dagger Functions can accept and return multiple different types, not just basic built-in types.
Update the Dagger Function to accept multiple input parameters (some of which are optional):
import { object, func } from "@dagger.io/dagger"
@object()
class Potato {
/**
* @param count The number of potatoes to process
* @param mashed Whether the potatoes are mashed
*/
@func()
helloWorld(count: number, mashed = false): string {
if (mashed) {
return `Hello Daggernauts, I have mashed ${count} potatoes`
}
return `Hello Daggernauts, I have ${count} potatoes`
}
}
You can use jsDoc to document the parameter in the API.
Here's an example of calling the Dagger Function with optional parameters:
dagger call hello-world --count=10 --mashed
The result will be:
Hello Daggernauts, I have mashed 10 potatoes
Use dagger call --help
at any point to get help on the commands and flags available.
Step 4: Add return values to your Dagger Function
Update the Dagger Function to return a custom PotatoMessage
type:
import { object, func, field } from "@dagger.io/dagger"
@object()
class Potato {
/**
* @param count The number of potatoes to process
* @param mashed Whether the potatoes are mashed
*/
@func()
helloWorld(count: number, mashed = false): PotatoMessage {
let m: string
if (mashed) {
m = `Hello Daggernauts, I have mashed ${count} potatoes`
} else {
m = `Hello Daggernauts, I have ${count} potatoes`
}
return new PotatoMessage(m, "potato@example.com")
}
}
@object()
class PotatoMessage {
@field()
message: string
@field()
from: string
constructor(message: string, from: string) {
this.message = message
this.from = from
}
}
Using the @field()
decorator is only needed to allow access to the field directly via the Dagger API. Otherwise, it will still be used during serialization/deserialization when passing the object instance to other functions.
Test it using dagger call
:
dagger call hello-world --count=10 message
dagger call hello-world --count=10 from
The result will be:
Hello Daggernauts, I have 10 potatoes
potato@example.com
Step 5: Create a more complex Dagger Function
Now, put everything you've learnt to the test, by building a Dagger Module and Dagger Function for a real-world use case: scanning a container image for vulnerabilities with Trivy.
-
Initialize a new module:
dagger init --name=trivy --sdk=typescript trivy
cd trivy -
Replace the generated
dagger/src/index.ts
file with the following code:import { dag, func, object } from "@dagger.io/dagger"
@object()
class Trivy {
@func()
async scanImage(
imageRef: string,
severity = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL",
exitCode = 0,
format = "table",
): Promise<string> {
return dag
.container()
.from("aquasec/trivy:latest")
.withExec([
"image",
"--quiet",
"--severity",
severity,
"--exit-code",
`${exitCode}`,
"--format",
format,
imageRef,
])
.stdout()
}
}In this example, the
scanImage()
function accepts four parameters:- A reference to the container image to be scanned (required);
- A severity filter (optional);
- The exit code to use if scanning finds vulnerabilities (optional);
- The reporting format (optional).
The function code performs the following operations:
- It uses the default
dag
client'scontainer().from()
method to initialize a new container from a base image. In this example, the base image is the official Trivy imageaquasec/trivy:latest
. This method returns aContainer
representing an OCI-compatible container image. - It uses the
Container.withExec()
method to define the command to be executed in the container - in this case, thetrivy image
command for image scanning. It also passes the optional parameters to the command. ThewithExec()
method returns a revisedContainer
with the results of command execution. - It retrieves the output stream of the command with the
Container.stdout()
method and prints the result to the console.
-
Test the function using
dagger call
:dagger call scan-image --image-ref alpine:latest
Here's an example of the output:
alpine:latest (alpine 3.19.1)
=============================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
Conclusion
This guide taught you the basics of writing a Dagger Module. It showed you how to initialize a Dagger Module, add Dagger Functions to it, and work with arguments and custom return values. It also worked through a real-world use case: a Dagger Module to scan container images with Trivy.
Continue your journey into Dagger programming with the following resources: