Back to Dagger

Module Structure

docs/versioned_docs/version-0.17.2/api/module-structure.mdx

0.21.519.1 KB
Original Source

import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import DaggerModuleInit from '../partials/_dagger_module_init.mdx';

Module Structure

<DaggerModuleInit />

File layout

Multiple files

<Tabs groupId="language"> <TabItem value="Go">

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).

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

go
// 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`
	...
}
</TabItem> <TabItem value="Python"> The Dagger module's code in Python can be split into multiple files by making a [package](https://docs.python.org/3/tutorial/modules.html#packages) 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:

python
# 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:

python
# 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()

:::important 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).

:::note 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. :::

:::tip 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. :::

:::tip SINGLE FILE MODULE 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:

<Tabs groupId="build-systems"> <TabItem value="hatchling">
toml
[build-system]
requires = ["hatchling>=0.15.0"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["main.py"]
</TabItem> <TabItem value="poetry-core">
toml
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
</TabItem> <TabItem value="setuptools">
toml
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
py-modules = ["main"]
</TabItem> </Tabs>

:::

:::tip CUSTOMIZE IMPORT PACKAGE 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:

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 in dagger.json, in PascalCase)
  • Default distribution package: my-module (in pyproject.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:

toml
[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.

:::

</TabItem> <TabItem value="TypeScript">

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:

typescript
// 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()
  }
}
</TabItem> <TabItem value="PHP">

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:

php
// 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();
    }
}
</TabItem> <TabItem value="Java"> 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 named MyModule.
  • The exposed objects must be annotated with @Object and the exposed functions with @Function.

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

java
/** My Dagger module */
@Module
package io.dagger.modules.mymodule;

import io.dagger.module.annotation.Module;

:::note 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.

java
/**
 * Returns the build container
 *
 * @param name The name of the container
 * @return The container
 */
@Function(description = "Build container")
public Container build(String name) {
    //...
}
console
$ dagger functions

Name    Description
build   Build container

::: </TabItem> </Tabs>

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:

  1. 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).
mermaid
graph TD
    A[Top-level orchestrator module] --> B[Webapp frontend module]
    A[Top-level orchestrator module] --> C[Webapp backend module]
    A[Top-level orchestrator module] --> D[Utilities module]
    A[Top-level orchestrator module] --> E[Docs module]

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.
  1. 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).
mermaid
graph TD
    A[Shared module] --> B[Microservice 1]
    A[Shared module] --> C[Microservice 2]
    A[Shared module] --> D[Microservice 3]

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.

<Tabs groupId="language"> <TabItem value="Go">

The runtime container is currently hardcoded to run in Go 1.21 (although this may be configurable in future).

</TabItem> <TabItem value="Python">

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:

shell
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.

:::tip

For more advanced needs, a different base image can be used by adding the following to your pyproject.toml:

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

</TabItem> <TabItem value="TypeScript">

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:

shell
# 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.

json
{
  "dependencies": {
    "typescript": "^5.3.2"
    "@dagger.io/dagger": "./sdk"
  }
}
</TabItem> <TabItem value="PHP">

The runtime container is currently hardcoded to run in php:8.3-cli-alpine (although this may be configurable in future).

</TabItem> <TabItem value="Java">

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).

</TabItem> </Tabs>

Language-native packaging

The structure of a Dagger module mimics that of each language's conventional packaging mechanisms and tools.

<Tabs groupId="language"> <TabItem value="Go">

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
// 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.

</TabItem> <TabItem value="Python">

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.

:::info 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:

shell
# 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:

shell
poetry export --without main -o requirements.lock

It will have to be manually kept in sync, though. :::

</TabItem> <TabItem value="TypeScript"> Dagger modules in Typescript are built to be installed, like libraries. The runtime container installs the module code with:
shell
# 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.

</TabItem> <TabItem value="PHP"> Dagger modules in PHP are built to be installed, like libraries. The runtime container installs the module code with:
shell
# 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.

</TabItem> <TabItem value="Java"> Dagger modules in Java are built as JAR files, using Maven. The runtime container builds the module code with:
shell
# executed by the runtime build container
mvn clean package

The generated JAR file is then executed by the runtime container with:

shell
# executed by the runtime run container
java -jar module.jar

:::important This means you have to keep the pom.xml file and especially the maven-compiler-plugin and the maven-shade-plugin configurations. :::

:::note Other build tools like Gradle might be supported in the future. ::: </TabItem> </Tabs>