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
.
├── LICENSE
├── dagger.gen.go
├── go.mod
├── go.sum
├── internal
│ ├── dagger
│ ├── querybuilder
│ └── telemetry
└── main.go
└── dagger.json
In this structure:
dagger.json
is the Dagger module configuration file.go.mod
/go.sum
manage the Go module and its dependencies.main.go
is where your Dagger module code goes. It contains sample code to help you get started.internal
contains automatically-generated types and helpers needed to configure and run the module:dagger
contains definitions for the Dagger API that's tied to the currently running Dagger Engine container.querybuilder
has utilities for building GraphQL queries (used internally by thedagger
package).telemetry
has 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.json
is the Dagger module configuration file.pyproject.toml
manages the Python project configuration.uv.lock
manages 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.json
is the Dagger module configuration file.package.json
manages 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.
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.
Module layout
- Go
- Python
- TypeScript
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()
}
}
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
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 21.3 (although this may be configurable in future).
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"
}
}
Language-native packaging
The structure of a Dagger module mimics that of each language's conventional packaging mechanisms and tools.
- Go
- Python
- TypeScript
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.