Module Structure
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.
File layoutβ
Multiple filesβ
- Go
- Python
- TypeScript
- PHP
- Java
You can split your Dagger module into multiple files, not just main.go. To do this, you can just create another file beside main.go (for example, utils.go):
.
βββ ...
βββ main.go
βββ utils.go
βββ dagger.json
This file should be inside the same package as main.go, and as such, can access any private variables/functions/types inside the package.
Additionally, you can also split your Dagger module into Go subpackages (for example, utils):
.
βββ ...
βββ main.go
|ββ utils
β βββ utils.go
βββ dagger.json
Because this is a separate package, you can only use the variables/functions/types that are exported from this package in main.go (you can't access types from main.go in the utils package).
Only types and functions in the top-level package are part of the public-facing API for the module.
You can access other Dagger types from a sub-package by importing the generated sub-package dagger/<module>/internal/dagger:
// utils/utils.go
import "dagger/<module>/internal/dagger"
func DoThing(client *dagger.Client) *dagger.Directory {
// we need to pass *dagger.Client in here, since we don't have access to `dag`
...
}
The Dagger module's code in Python can be split into multiple files by making a package and ensuring the main object is imported in __init__.py. All the other object types should already be imported from there.
For example given this directory structure:
.
βββ dagger.json
βββ src
β βββ my_module
β β βββ __init__.py
β β βββ main.py
β β βββ test.py
β β βββ lint.py
The __init__.py file should import the main object from main.py:
# src/my_module/__init__.py
"""My very own Dagger module"""
from .main import MyModule as MyModule
And the main.py file should import the other objects from their respective files:
# src/my_module/main.py
import dagger
from .test import Test # in src/my_module/test.py
from .lint import Lint # in src/my_module/lint.py
@dagger.object_type
class MyModule:
@dagger.function
def test(self) -> Test:
return Test()
@dagger.function
def lint(self) -> Lint:
return Lint()
Dagger expects that a Python Dagger module is structured like a library, so that the SDK is able to load the code with an import, but it's up to the Python build system to know where files are located in order to build and install the Python package correctly.
This affords a lot of flexibility in how Dagger Python modules can be structured, and which tools are supported.
The default project template follows known conventions for structuring a Python library (src layout, package name matching project name), which allows Python build backends to automatically recognize where the files are.
However, it's possible to change the project's name and file structure with a bit of extra configuration, as long as the build backend correctly builds the code in a way that allows the SDK to import it after installation (i.e., must be installed in site-packages).
Build backends are independent from installers (or build frontends, see PEP 517), even though some installers may provide both (as separate packages). For example, hatch vs hatchling, poetry vs poetry-core.
dagger init won't override existing pyproject.toml and .py files, so it's possible to use an external process to generate a different template, before calling dagger init.
Here is an example of moving all the code into a single main.py module, resulting in the following structure:
.
βββ dagger.json
βββ main.py
βββ pyproject.toml
βββ uv.lock
And corresponding pyproject.toml configuration:
- hatchling
- poetry-core
- setuptools
[build-system]
requires = ["hatchling>=0.15.0"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["main.py"]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
py-modules = ["main"]
The Python SDK looks for the main object of the Dagger module in the Python import package that is named after the distribution package name (in particular, using underscores _ as a word separator).
If they are different, you must explicitly tell the Python SDK where the main object needs to be imported from, using the following entry point configuration in pyproject.toml:
[project.entry-points."dagger.mod"]
main_object = "<import package>:<main object>"
For example, for a Dagger module named my-module:
- Main object:
MyModule(required to be the name indagger.json, in PascalCase) - Default distribution package:
my-module(inpyproject.toml; can be changed) - Default import package:
src/my_module(normalized after distribution package name; can be changed)
Then, the default main_object entry point that the Python SDK looks for is my_module:MyModule, with a fallback to main:MyModule for backwards compatibility.
Thus, if you have the following configuration:
[project.entry-points."dagger.mod"]
main_object = "my_module.main:MyModule"
Then the import in __init__.py is no longer needed since Dagger knows to import from my_module.main directly.
Due to TypeScript limitations, it is not possible to split your main class module (index.ts) into multiple files. However, it is possible to create sub-classes in different files and access them from your main class module:
// src/index.ts
import { func, object } from "@dagger.io/dagger"
import { Test } from "./test" // in src/test.ts
import { Lint } from "./lint" // in src/lint.ts
@object()
class MyModule {
@func()
test(): Test {
return new Test()
}
@func()
lint(): Lint {
return new Lint()
}
}
Only functions from your main class (MyModule.php) can initially be called by Dagger. However, it is possible to create other classes and access them from your main class:
// src/MyModule.php
<?php
declare(strict_types=1);
namespace DaggerModule;
use Dagger\Attribute\{DaggerObject, DaggerFunction};
use DaggerModule\Test; // in src/Test.php
use DaggerModule\Lint; // in src/Lint.php
use function Dagger\dag;
#[DaggerObject]
class MyModule
{
#[DaggerFunction]
public function test(): Test
{
return new Test();
}
#[DaggerFunction]
public function lint(): Lint
{
return new Lint();
}
}
The Dagger module's code in Java can be split into multiple classes, in multiple files. A few constraints apply:
- The main Dagger object must be represented by a class using the same name as the module, in PascalCase. For instance if the module name is
my-module, the main object's class must be namedMyModule. - The exposed objects must be annotated with
@Objectand the exposed functions with@Function.
The package in which the main object exists is not important. By convention, the name is based on the module name, but any name may be used.
The description of the module can be set in a package-info.java file, with the @Module annotation:
/** My Dagger module */
@Module
package io.dagger.modules.mymodule;
import io.dagger.module.annotation.Module;
By default, the description will be read from the JavaDoc documentation of the package, class or function. To define a different
description, use the description field in each @Module, @Object or @Function annotation.
/**
* Returns the build container
*
* @param name The name of the container
* @return The container
*/
@Function(description = "Build container")
public Container build(String name) {
//...
}
$ dagger functions
Name Description
build Build container
Monoreposβ
A monorepo typically contains multiple independent projects, each of which has different test, build and deployment requirements. Managing these requirements in a single CI workflow or YAML file can be incredibly complex and time-consuming.
Dagger modules provide a framework that you can use to break up this complexity and cleanly separate CI responsibilities in a monorepo without losing reusability or performance. There are two possible patterns you can follow:
- One top-level Dagger module per project, with sub-modules for the various CI workflows in that project. This pattern is suitable when there are significant differences between the projects in the monorepo (e.g. a monorepo with SDKs, CLIs, web applications, docs, all of which have different CI requirements).
Benefits of this pattern include:
- Easier debugging: Sub-modules provide a way to separate, and therefore easily debug, the business logic for different CI tasks.
- Code reuse: There may be opportunities for sub-modules in different projects to import each other to reuse existing functionality.
- Improved performance: The top-level module of a project can orchestrate the sub-modules using the languageβs native concurrency features.
- A single, shared CI / automation module which all projects use and contribute to. This pattern is suitable when there are significant commonalities between the projects in the monorepo (e.g. a monorepo with only micro-services or only front-end applications).
Benefits of this pattern include:
- Code reuse: This reduces code duplication and ensures a consistent CI environment for all projects. For example, the shared module could create a common build environment and leverage this for multiple projects in the monorepo.
- Reduced onboarding friction: There is no need to create a new CI module when adding a new project or component. New projects can get started faster with their CI implementation.
- Best practices: All projects benefit from the best practices implemented in the shared module.
- Knowledge sharing: By contributing to a shared CI module, project teams can learn from each other's CI strategies.
Runtime containerβ
Dagger modules run in a runtime container that's bootstrapped by the Dagger Engine, with the necessary environment to run the Dagger module's code.
- Go
- Python
- TypeScript
- PHP
- Java
The runtime container is currently hardcoded to run in Go 1.21 (although this may be configurable in future).
The runtime container is based on the python:3.12-slim base image by default, but it can be overridden by setting requires-python, or pinned with a .python-version file next to your pyproject.toml:
echo "3.11" > .python-version
This will instruct Dagger to use the python:3.11-slim base image instead.
Pinning the interpreter version can be useful to prevent an automatic upgrade from a future version of Dagger, or to select a newer version.
For more advanced needs, a different base image can be used by adding the following to your pyproject.toml:
[tool.dagger]
base-image = "acme/python:3.11"
This can be useful to add a few requirements to the module's execution environment such as system packages like git, or to add necessary environment variables, for example. However, don't deviate from the default base image too much or it may break in a future version of Dagger.
β οΈ Override at own risk!
The runtime container is currently hardcoded to run in Node.js 22.11.0, but it can be overridden by setting an alternative base image.
Bun is experimentally supported and work is in progress to support Deno.
The TypeScript SDK is installed automatically, including dependencies, with a version that's tied to the currently running Dagger Engine container:
# executed by the runtime container
yarn install
npm pkg set "dependencies[@dagger.io/dagger]=./sdk"
The SDK files are mounted under /sdk in the Dagger Engine runtime container.
This is why the initial package.json doesn't include any dependencies except a local link to the sdk generated directory.
{
"dependencies": {
"typescript": "^5.3.2"
"@dagger.io/dagger": "./sdk"
}
}
The runtime container is currently hardcoded to run in php:8.3-cli-alpine (although this may be configurable in future).
Two containers are used by the runtime, one to build the module into a JAR file, one to run it.
They are currently hardcoded to run in maven:3.9.9-eclipse-temurin-17 and eclipse-temurin:23-jre-noble respectively (although this may be configurable in future).
Language-native packagingβ
The structure of a Dagger module mimics that of each language's conventional packaging mechanisms and tools.
- Go
- Python
- TypeScript
- PHP
- Java
Dagger modules written for use with the Go SDK are automatically created as Go modules. At module creation time, a go.mod and go.sum file will automatically be created that import the necessary dependencies. Dependencies can be installed and managed just as for any standard Go environment.
After using new dependencies in your code, update your go.mod/go.sum with the newly imported dependencies by using go mod tidy.
Go workspacesβ
Since it's common to have a sub-directory inside your main project containing your Dagger module code, you can manage your modules using Go workspaces.
When a new Dagger module is created, Dagger attempts to add it to a root go.work if it exists. If not, it can be added manually later with go work use ./path/to/mymodule.
// go.work
go 1.21.7
use (
./path/to/mymodule
)
Go vendorβ
Go vendor directories are not currently supported. See https://github.com/dagger/dagger/issues/6076 for more information.
Dagger modules in Python are built to be installed, like libraries. At module creation time, a pyproject.toml and uv.lock file will automatically be created that depend on the locally generated client library (in ./sdk). This dependency is configured to be editable so that changes in the code don't require a re-install.
With the uv.lock file, Dagger uses uv's project management capabilites, but it can be opted out by removing this file, in which case Dagger falls back to the pip interface instead.
Dagger also supports pinning dependencies with a pip-tools compatible requirements.lock file in order to support the use of other project managers like Poetry or Hatch locally (when developing).
In this case, Dagger installs dependencies with:
# executed by the runtime
uv pip install -r requirements.lock -e ./sdk -e .
Notice that the ./sdk and . packages don't need to be in the requirements.lock file, only the third party dependencies.
This means module developers can use any tool they want to manage their virtual environment and install dependencies, but if third-party dependencies aren't pinned in a requirements.lock file, the developers may get different versions between the Dagger execution environment and their own local environment.
For example, Poetry has its own poetry.lock which Dagger doesn't recognize, but it can be exported as a requirements.lock file with:
poetry export --without main -o requirements.lock
It will have to be manually kept in sync, though.
Dagger modules in Typescript are built to be installed, like libraries. The runtime container installs the module code with:
# executed by the runtime container
yarn install --production
This means that so long as the project has a package.json file, it can be used as a library and it can be managed using any Node.js package manager such as npm, pnpm or yarn.
Only production dependencies are installed, not packages defined in the devDependencies field.
Dagger modules in PHP are built to be installed, like libraries. The runtime container installs the module code with:
# executed by the runtime container
composer install
This means that so long as the project has a composer.json file, it can be used as a library.
Dagger modules in Java are built as JAR files, using Maven. The runtime container builds the module code with:
# executed by the runtime build container
mvn clean package
The generated JAR file is then executed by the runtime container with:
# executed by the runtime run container
java -jar module.jar
This means you have to keep the pom.xml file and especially the maven-compiler-plugin and the maven-shade-plugin configurations.
Other build tools like Gradle might be supported in the future.