Skip to main content

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:

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!
note

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
tip

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.

  1. Initialize a new module:

    dagger init --name=trivy --sdk=typescript trivy
    cd trivy
  2. 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's container().from() method to initialize a new container from a base image. In this example, the base image is the official Trivy image aquasec/trivy:latest. This method returns a Container 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, the trivy image command for image scanning. It also passes the optional parameters to the command. The withExec() method returns a revised Container 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.
  3. 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: