Go SDK
The Go SDK lets you write Dagger modules in Go. You define plain Go structs and methods; the SDK turns them into Dagger objects and functions that anyone can call from the CLI, from another module, or over the API. In return, your module gets a generated, fully-typed Go client (dag) for the entire Dagger API — containers, directories, files, secrets, services, and every module you depend on.
This page is a standalone guide to the Go SDK. It assumes you already understand the platform concepts covered in the Module Developer Guide:
- Type system — how SDK types map to the Dagger API
- Checks — functions that validate your code
- Changesets — how generators and tooling return diffs for you to apply
A useful, reusable module provides at least one of the three first-class function types — a check, a generator, or a service. See Checks, generators, services, directives, and ignore patterns for the Go syntax.
The one thing that is genuinely different from older Dagger versions: module development tooling is not in the dagger CLI. There is no dagger init or dagger develop. Instead, the Go SDK is itself a Dagger module — github.com/dagger/go-sdk — and you scaffold, generate, and maintain Go modules by calling its functions:
dagger -m github.com/dagger/go-sdk call <function> ...
Create a module​
Run init from inside a Git repository — that's where the new module is created.
Create a new Go module with the SDK module's init function. The only required argument is --name:
dagger -m github.com/dagger/go-sdk call init --name my-module
init does not write files directly. Like every Dagger tool that modifies your workspace (for example dagger call prettier write, or a module's generators), it returns a changeset — a structured diff of files to create. You then choose how to apply it.
Inspect what it would write:
# Show the proposed diff as a patch
dagger -m github.com/dagger/go-sdk call init --name my-module as-patch
# List just the paths
dagger -m github.com/dagger/go-sdk call init --name my-module added-paths
Apply it directly (review, or pass -y to apply without prompting):
dagger -m github.com/dagger/go-sdk call init --name my-module
Dagger shows the changeset and applies it after you confirm, so you can review the diff before anything touches your filesystem. Pass -y / --auto-apply to skip the prompt.
Where the module is created​
By default, init places the new module under the nearest .dagger directory visible from your current workspace path:
<nearest .dagger>/modules/<name>
Pass --path to choose a different location (the target must not already contain a Dagger module):
dagger -m github.com/dagger/go-sdk call init --name my-module --path ./ci
Pass --template to materialize a non-default starter from the SDK module's templates/<template> directory. The empty default uses the minimal template. List available templates with:
dagger -m github.com/dagger/go-sdk call templates
By default the generated SDK files are checked into version control. Pass --ignore-generated to instead configure generation to add those paths to .gitignore:
dagger -m github.com/dagger/go-sdk call init --name my-module --ignore-generated
Resulting file layout​
A freshly-initialized Go module looks like this:
my-module/
├── dagger.json
├── go.mod
├── go.sum
├── main.go # your code
├── dagger.gen.go # generated: top-level helpers (committed)
├── .gitattributes # marks generated files for linguist
└── internal/
├── dagger/ # generated: the typed Dagger client (committed)
│ └── dagger.gen.go
├── querybuilder/ # generated
└── telemetry/ # generated
The dagger.json records that this is a Go SDK module via sdk.source:
{
"name": "my-module",
"engineVersion": "v0.21.0",
"sdk": {
"source": "go"
}
}
sdk.source: "go" is what makes this a Go module — it tells the engine to use the Go runtime and is how the SDK module's modules function discovers Go modules in a workspace. The generated files in dagger.gen.go and internal/ are not handwritten; see Regenerate bindings.
You can list the Go modules in a workspace with dagger -m github.com/dagger/go-sdk call modules — it returns every module whose sdk.source is "go".
Define objects and functions​
A Go module is an ordinary Go package named main. The main object is a struct whose name matches your module (Dagger PascalCases the module name): a module named my-module has a MyModule struct. Every exported method on that struct becomes a callable Dagger Function.
// A simple example module to say hello.
// Further documentation for the module here.
package main
import (
"fmt"
"strings"
)
type MyModule struct{}
// Return a greeting.
func (m *MyModule) Hello(
// Who to greet
name string,
// The greeting to display
greeting string,
) string {
return fmt.Sprintf("%s, %s!", greeting, name)
}
// Return a loud greeting.
func (m *MyModule) LoudHello(
// Who to greet
name string,
// The greeting to display
greeting string,
) string {
out := fmt.Sprintf("%s, %s!", greeting, name)
return strings.ToUpper(out)
}
Key rules:
- The package is always
package main. - Method receivers may be value (
func (m MyModule)) or pointer (func (m *MyModule)) — be consistent within a type. - Exported (capitalized) methods become Dagger Functions. Unexported methods are private Go helpers, invisible to callers.
- The first parameter may be a
context.Context; it does not appear as a Dagger argument and is supplied by the runtime. - A method may return an error as its last value; a non-nil error fails the function.
Call your functions exactly like any other module — from the directory that contains dagger.json (or with -m <path>):
dagger call hello --name=World --greeting=Hello
# Hello, World!
dagger call loud-hello --name=World --greeting=Hello
# HELLO, WORLD!
Go method and argument names are converted to kebab-case on the CLI (LoudHello → loud-hello, name → --name).
The constructor​
If you define a New function in the same package, it becomes the module's constructor. Its arguments become arguments of the main object, and its return value is the initialized main object. Use it for module-wide configuration and shared state.
A common pattern is to accept a *dagger.Workspace so the module can read the project it runs against (see Workspace inputs). Dagger auto-populates it from the current workspace, and content is pulled lazily, so you store the project directory once and reuse it:
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct {
Source *dagger.Directory
}
func New(
// The current workspace, auto-populated by Dagger.
ws *dagger.Workspace,
) *MyModule {
return &MyModule{
// Read the workspace root; nothing is uploaded until a function uses it.
Source: ws.Directory("/"),
}
}
func (m *MyModule) Foo(ctx context.Context) ([]string, error) {
return dag.Container().
From("alpine:latest").
WithMountedDirectory("/app", m.Source).
Directory("/app").
Entries(ctx)
}
Exported struct fields (like Source above) are part of the object's state and are serialized between functions in a chain. Mark a field private to Dagger — present in Go but hidden from the API — with a +private comment:
type LintRun struct {
// +private
Source *dagger.Directory
}
Arguments and return values​
Dagger derives a function's argument and return types from the Go signature. The mapping is:
| Go type | Dagger type |
|---|---|
string | String |
int | Int |
float64 | Float |
bool | Boolean |
[]T | [T] (list) |
*dagger.Directory | Directory |
*dagger.File | File |
*dagger.Container | Container |
*dagger.Secret | Secret |
*dagger.Service | Service |
a custom struct T | object T |
a custom string type with consts | enum |
Documentation​
Doc comments become API documentation, surfaced in dagger functions and dagger call --help. A comment directly above a method documents the function; a comment directly above a parameter documents that argument; a comment above the package main declaration documents the whole module.
// Return a greeting.
func (m *MyModule) Hello(
// Who to greet
name string,
) string {
return fmt.Sprintf("Hello, %s!", name)
}
Optional and default arguments​
Dagger arguments are required by default. Make one optional, or give it a default, with a magic comment on the parameter:
func (m *MyModule) Hello(
ctx context.Context,
// +optional
name string,
) (string, error) {
if name != "" {
return fmt.Sprintf("Hello, %s", name), nil
}
return "Hello, world", nil
}
func (m *MyModule) Hello(
ctx context.Context,
// +default="world"
name string,
) (string, error) {
return fmt.Sprintf("Hello, %s", name), nil
}
+optionalmakes the argument optional. For scalar types it defaults to the Go zero value; for pointer types (*dagger.Directory, etc.) it defaults tonil, so you can detect "not passed."+default="..."makes the argument optional and supplies a default value when the caller omits it.
Nullability​
Use a pointer to a core type (e.g. *dagger.Secret) when an argument or return value may be absent. A nil pointer corresponds to a null/omitted value. Non-pointer scalars (string, int, bool) are always present.
Enums​
Model a closed set of string values as a named string type with a block of typed constants. Dagger turns it into an enum and validates inputs:
package main
import "context"
type MyModule struct{}
// Vulnerability severity levels
type Severity string
const (
// Undetermined risk; analyze further.
Unknown Severity = "UNKNOWN"
// Minimal risk; routine fix.
Low Severity = "LOW"
// Moderate risk; timely fix.
Medium Severity = "MEDIUM"
// Serious risk; quick fix needed.
High Severity = "HIGH"
// Severe risk; immediate action.
Critical Severity = "CRITICAL"
)
func (m *MyModule) Scan(ctx context.Context, ref string, severity Severity) (string, error) {
return dag.Container().
From("aquasec/trivy:0.50.4").
WithExec([]string{
"trivy", "image",
"--severity=" + string(severity),
ref,
}).Stdout(ctx)
}
Passing a value outside the enum produces a clear error listing the allowed choices:
dagger call scan --ref=alpine:latest --severity=FOO
# Error: value should be one of UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL
Custom object types​
Return a struct to expose a custom object. Exported fields become readable values, and exported methods on the type become chainable functions. Dagger prefixes custom type names with the module name in the API schema (e.g. MyModuleOrganization) to avoid collisions:
package main
import "dagger/my-module/internal/dagger"
type MyModule struct{}
func (module *MyModule) DaggerOrganization() *Organization {
url := "https://github.com/dagger"
return &Organization{
URL: url,
Repositories: []*dagger.GitRepository{dag.Git(url + "/dagger")},
Members: []*Account{
{"jane", "jane@example.com"},
{"john", "john@example.com"},
},
}
}
type Organization struct {
URL string
Repositories []*dagger.GitRepository
Members []*Account
}
type Account struct {
Username string
Email string
}
func (account *Account) URL() string {
return "https://github.com/" + account.Username
}
This enables chaining on the CLI and API:
dagger call dagger-organization members url
Interfaces​
Interfaces let your module accept arbitrary objects from other modules without depending on their concrete types. Declare a Go interface that embeds DaggerObject and lists the functions you need; any object that provides matching functions can be passed in:
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
type Fooer interface {
DaggerObject
Foo(ctx context.Context, bar int) (string, error)
}
func (m *MyModule) Foo(ctx context.Context, fooer Fooer) (string, error) {
return fooer.Foo(ctx, 42)
}
The DaggerObject marker (provided by the generated client) is required so Dagger knows this interface describes a Dagger object rather than a plain Go interface.
Working with core Dagger types​
The generated client exposes the entire Dagger API through a package-level variable named dag. You use it to build containers, mount directories and files, handle secrets, and run services. Core types live in the internal/dagger package, imported as dagger.
Containers​
Each builder method returns a new, immutable *dagger.Container. Nothing mutates in place, and every step is content-addressed and cached automatically.
package main
import (
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Build and return a container
func (m *MyModule) Build(source *dagger.Directory) *dagger.Container {
return dag.Container().
From("node:20").
WithDirectory("/app", source).
WithWorkdir("/app").
WithExec([]string{"npm", "install"}).
WithExec([]string{"npm", "run", "build"})
}
Directories and files​
*dagger.Directory and *dagger.File are first-class, "just-in-time" artifacts you can accept as arguments, produce as return values, mount into containers, and export to the host. Some builder methods take an options struct for less-common parameters — for example WithDirectory accepts dagger.ContainerWithDirectoryOpts:
func (m *MyModule) CopyDirectoryWithExclusions(
ctx context.Context,
// Source directory
source *dagger.Directory,
// Exclusion pattern
// +optional
exclude []string,
) *dagger.Container {
return dag.Container().
From("alpine:latest").
WithDirectory("/src", source, dagger.ContainerWithDirectoryOpts{Exclude: exclude})
}
The pattern is consistent: required parameters are positional Go arguments, optional ones live in a generated XxxOpts struct.
Workspace inputs​
When a module needs to read the user's project — its source tree, config files, lockfiles — it takes a *dagger.Workspace argument, almost always on the constructor. You don't pass it: Dagger auto-populates it from the current workspace, and nothing is uploaded up front. Project content is pulled lazily, on demand when a function actually reads a path, so a module can declare access to the whole workspace cheaply and only pay for what it touches.
package main
import (
"dagger/my-module/internal/dagger"
)
type MyModule struct {
Source *dagger.Directory
}
func New(
// The current workspace, auto-populated by Dagger.
ws *dagger.Workspace,
) *MyModule {
return &MyModule{
// Pull the workspace root as a Directory (lazy — no upload yet).
Source: ws.Directory("/"),
}
}
// Functions reuse the pulled Directory like any other.
func (m *MyModule) Build() *dagger.Container {
return dag.Container().
From("node:20").
WithDirectory("/app", m.Source).
WithWorkdir("/app").
WithExec([]string{"npm", "install"}).
WithExec([]string{"npm", "run", "build"})
}
The Workspace client type exposes accessors for reading project content:
| Accessor | Signature | Returns |
|---|---|---|
Directory | ws.Directory(path string, opts ...dagger.WorkspaceDirectoryOpts) *dagger.Directory | a Directory at path |
File | ws.File(path string) *dagger.File | a File at path |
FindUp | ws.FindUp(ctx, name string, opts ...dagger.WorkspaceFindUpOpts) (string, error) | the workspace path of name, searching upward |
Path resolution. A relative path resolves from the workspace's current working directory; an absolute path (starting with /) resolves from the workspace root (boundary). So ws.Directory("/") is the whole project root, while ws.Directory(".") is wherever the user invoked Dagger from.
Excluding files. Directory takes a dagger.WorkspaceDirectoryOpts options struct to filter what gets pulled. Tight filters matter for cache efficiency — loading less means fewer cache invalidations:
func New(ws *dagger.Workspace) *MyModule {
return &MyModule{
Source: ws.Directory("/", dagger.WorkspaceDirectoryOpts{
Exclude: []string{"node_modules", ".git", "dist"},
// Include: []string{"app/", "package.*"}, // allowlist instead
// Gitignore: true, // apply .gitignore rules
}),
}
}
FindUp walks up from a start path (relative paths resolve from the workspace cwd; pass dagger.WorkspaceFindUpOpts{From: "..."} to change it) and returns the absolute workspace path of the first match, stopping at the workspace boundary. Use it to locate a project root marker such as package.json or go.mod.
A function that doesn't take a *dagger.Workspace argument can still reach the current workspace through the client with dag.CurrentWorkspace(), which returns the same *dagger.Workspace type.
Secrets​
Accept sensitive values as *dagger.Secret, never as plain strings. Dagger scrubs secret plaintext from logs, caches, and crash reports:
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Query the GitHub API
func (m *MyModule) GithubApi(
ctx context.Context,
// GitHub API token
token *dagger.Secret,
) (string, error) {
return dag.Container().
From("alpine:3.17").
WithSecretVariable("GITHUB_API_TOKEN", token).
WithExec([]string{"apk", "add", "curl"}).
WithExec([]string{"sh", "-c", `curl "https://api.github.com/repos/dagger/dagger/issues" --header "Authorization: Bearer $GITHUB_API_TOKEN"`}).
Stdout(ctx)
}
Callers supply secrets through providers on the CLI:
dagger call github-api --token=env:GITHUB_TOKEN # environment variable
dagger call github-api --token=file:./token.txt # file
dagger call github-api --token=cmd:"gh auth token" # command output
dagger call github-api --token=op://vault/item/field # 1Password
Services​
Return *dagger.Service to expose a long-running service, and bind it into other containers with WithServiceBinding. Services are content-addressed, so a given definition always gets the same hostname — no port conflicts:
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Start and return an HTTP service
func (m *MyModule) HttpService() *dagger.Service {
return dag.Container().
From("python").
WithWorkdir("/srv").
WithNewFile("index.html", "Hello, world!").
WithExposedPort(8080).
AsService(dagger.ContainerAsServiceOpts{Args: []string{"python", "-m", "http.server", "8080"}})
}
// Send a request to an HTTP service and return the response
func (m *MyModule) Get(ctx context.Context) (string, error) {
return dag.Container().
From("alpine").
WithServiceBinding("www", m.HttpService()).
WithExec([]string{"wget", "-O-", "http://www:8080"}).
Stdout(ctx)
}
A larger example​
Real modules combine these pieces. This is adapted from Dagger's own ruff module (a Go SDK module in the Dagger repo) — note custom types, chaining, error returns, and the use of dag.CurrentModule() to reach the module's own source:
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/dagger/dagger/modules/ruff/internal/dagger"
)
// Ruff is a fast Python linter implemented in Rust
type Ruff struct{}
// Lint a Python codebase
func (ruff Ruff) Lint(
// The Python source directory to lint
source *dagger.Directory,
) *LintRun {
return &LintRun{Source: source}
}
// The result of running the Ruff lint tool
type LintRun struct {
// +private
Source *dagger.Directory
}
// Return a JSON report file for this run
func (run LintRun) Report() *dagger.File {
cmd := []string{"/ruff", "check", "--exit-zero", "--output-format", "json", "."}
return dag.
CurrentModule().
Source().
Directory("build").
DockerBuild().
WithMountedDirectory("", run.Source).
WithExec(cmd, dagger.ContainerWithExecOpts{RedirectStdout: "ruff-report.json"}).
File("ruff-report.json")
}
// Return an error if the run reported any issues
func (run LintRun) Assert(ctx context.Context) error {
contents, err := run.Report().Contents(ctx)
if err != nil {
return err
}
var issues []struct {
Message string `json:"message"`
}
if err := json.Unmarshal([]byte(contents), &issues); err != nil {
return err
}
if len(issues) > 0 {
var lines []string
for _, i := range issues {
lines = append(lines, " - "+i.Message)
}
return errors.New(fmt.Sprintf("%d issues\n%s", len(issues), strings.Join(lines, "\n")))
}
return nil
}
Module dependencies​
A module can depend on other Dagger modules and call them through dag — for example dag.Ruff() once ruff is a dependency. Dependencies are recorded in dagger.json under dependencies:
{
"name": "dev",
"engineVersion": "v0.21.0",
"sdk": { "source": "go" },
"dependencies": [
{ "name": "go", "source": "../../toolchains/go" },
{ "name": "wolfi", "source": "../wolfi" }
]
}
A source may be a local path (../wolfi) or a remote reference of the form [proto://]host/repo[/subpath][@version], e.g. github.com/shykes/daggerverse/hello@v0.3.0.
Manage dependencies with the SDK module's mod deps operations rather than hand-editing dagger.json. Each returns a changeset you apply directly (review, or pass -y to apply without prompting).
Add a dependency (--source required, --name optional alias):
dagger -m github.com/dagger/go-sdk call mod deps add \
--source github.com/shykes/daggerverse/hello@v0.3.0
List the current dependencies (returns names where present, otherwise sources):
dagger -m github.com/dagger/go-sdk call mod deps list
Update one dependency by name or source, or all remote dependencies when --name is omitted:
# Update a single dependency
dagger -m github.com/dagger/go-sdk call mod deps update --name hello
# Update all remote dependencies
dagger -m github.com/dagger/go-sdk call mod deps update
Remove a dependency by name (--name required):
dagger -m github.com/dagger/go-sdk call mod deps remove --name hello
After changing dependencies, regenerate bindings so the new module's functions appear on dag.
By default, mod deps resolves the module at or above your current workspace path. Pass --path to target a specific module, and --find-up to allow --path to point inside the module rather than at its root.
Regenerate bindings and generated files​
A Go module ships with generated code that you commit alongside your handwritten main.go:
dagger.gen.go— top-level helpersinternal/dagger/— the typed Dagger client, includingdag, all core types (Container,Directory, …), every dependency's functions, and theXxxOptsoption structsinternal/querybuilder/,internal/telemetry/— supporting generated code
These files are generated, not authored. They are marked in .gitattributes as linguist-generated so platforms like GitHub collapse them in diffs:
/dagger.gen.go linguist-generated
/internal/dagger/** linguist-generated
/internal/querybuilder/** linguist-generated
/internal/telemetry/** linguist-generated
Regenerate them whenever you change your module's functions, bump the engine version, or add/update/remove a dependency. Use the SDK module's mod generate, which returns a changeset:
# Review the regenerated files, then apply
dagger -m github.com/dagger/go-sdk call mod generate
You can also regenerate every Go module discovered in the workspace at once:
dagger -m github.com/dagger/go-sdk call generate-all
dagger generate (the user-facing command described in Generating Code) discovers and runs your module's generators, which for a Go module includes this binding regeneration. Both paths produce a changeset you review and apply.
A module (or an ancestor) can carry a "skip generate" marker; when present, mod generate returns an empty changeset. Check whether a marker is in effect with dagger -m github.com/dagger/go-sdk call mod skip-generate.
Whether generated SDK files are committed or git-ignored is controlled per-module. It is set when you run init (the --ignore-generated flag) and recorded in dagger.json under codegen (e.g. "codegen": { "automaticGitignore": false }). Committing them is the default and is recommended so consumers can build your module without first running codegen.
Engine version​
Each module declares the Dagger engine version it requires in dagger.json (engineVersion). Manage it with the SDK module's mod engine operations.
Read the currently required version (without the leading v):
dagger -m github.com/dagger/go-sdk call mod engine required
Pin a specific version, the current engine, or the latest stable release (each returns a changeset):
# A specific version (--version required)
dagger -m github.com/dagger/go-sdk call mod engine require --version v0.21.0
# Whatever engine you're running now
dagger -m github.com/dagger/go-sdk call mod engine require-current
# Latest stable release
dagger -m github.com/dagger/go-sdk call mod engine require-latest
Bumping the engine version usually means the generated bindings should change too, so follow with mod generate.
Checks, generators, services, directives, and ignore patterns​
A useful, reusable module provides at least one of the three first-class function types — a check, a generator, or a service — so that the platform verbs (dagger check, dagger generate, dagger up) have something to run. These work the same in Go as in any SDK; see the Module Developer Guide for the full treatment. In Go, you mark each one with a doc-comment pragma on the function:
| Pragma | Return type | Run by | Purpose |
|---|---|---|---|
// +check | error (or *dagger.Container) | dagger check | validate the project (test/lint/scan) |
// +generate | *dagger.Changeset | dagger generate | produce a diff to apply to the workspace |
// +up | *dagger.Service | dagger up | start a long-running service |
The Go-specific surface is:
Magic comment directives​
Go modules use // +directive comments to add Dagger metadata that Go's type system can't express:
| Directive | Placement | Meaning |
|---|---|---|
// +optional | above an argument | argument is optional |
// +default="x" | above an argument | optional with a default value |
// +private | above a struct field | keep the field out of the API |
// +check | above a method | mark the function as a check |
// +generate | above a method | mark the function as a generator |
// +up | above a method | mark the function as a service |
Ignore patterns​
A module reads the user's project through a *dagger.Workspace argument (see Workspace inputs), not a path-defaulted *dagger.Directory. To filter what gets pulled, use the exclude option when reading a workspace directory. Tight filters are essential for cache efficiency (load less → fewer cache invalidations):
func New(ws *dagger.Workspace) *MyModule {
return &MyModule{
Source: ws.Directory("/", dagger.WorkspaceDirectoryOpts{
Exclude: []string{"node_modules", ".git", "dist"},
}),
}
}
Checks​
Mark a function with the // +check pragma to make it a check — a validation function (test, lint, scan) that takes no caller arguments. dagger check discovers and runs every check a module exposes. A check fails when it returns a non-nil error, or when it returns a *dagger.Container whose execution exits non-zero. The platform model is described in Checks.
// Lint the project.
//
// +check
func (m *MyModule) Lint(ctx context.Context) error {
_, err := dag.Container().
From("golangci/golangci-lint:latest").
WithMountedDirectory("/src", m.Source).
WithWorkdir("/src").
WithExec([]string{"golangci-lint", "run"}).
Sync(ctx)
return err
}
// A check can also return a container; a non-zero exit fails the check.
//
// +check
func (m *MyModule) Build() *dagger.Container {
return dag.Container().From("alpine:3").WithExec([]string{"true"})
}
The pragma may be placed on its own line within the doc comment (a blank // line separates the human-readable description from the pragma, as shown above). Checks can also be declared on custom object types, so you can group them (for example a Test object with Lint and Unit checks).
Generators​
Mark a function with the // +generate pragma to make it a generator. It returns a *dagger.Changeset: it runs a tool, captures the resulting directory, and diffs it against the source. dagger generate discovers and runs every generator and presents the combined changeset for you to review and apply.
// Format the source.
//
// +generate
func (m *MyModule) Format() *dagger.Changeset {
formatted := dag.Container().
From("golang:latest").
WithMountedDirectory("/src", m.Source).
WithWorkdir("/src").
WithExec([]string{"gofmt", "-w", "."}).
Directory("/src")
return formatted.Changes(m.Source)
}
This author-written // +generate function is distinct from regenerating the SDK's own client bindings. Binding regeneration is the SDK module's mod generate operation, covered under Regenerate bindings; the user-facing dagger generate runs both your // +generate functions and (for a Go module) the binding regeneration.
See Generating Code and Changesets.
Services​
Mark a function with the // +up pragma to make it a service. It returns a *dagger.Service, and dagger up discovers and starts every service the module exposes, exposing their ports on the host.
// Run the web server.
//
// +up
func (m *MyModule) Web() *dagger.Service {
return dag.Container().
From("nginx:alpine").
WithExposedPort(80).
AsService()
}
Like checks, // +up services can also be declared on custom object types so you can group related services (for example an Infra object exposing a Database service). This is the same *dagger.Service type described under Services above; the // +up pragma is what makes a service function runnable directly via dagger up.
Testing Go modules​
Because a Go module is ordinary Go, you have two complementary testing strategies.
Idiomatic Go tests​
Functions that contain pure Go logic — parsing reports, formatting summaries, computing counts — can be unit-tested with the standard testing package, with no engine involved:
package main
import "testing"
func TestIssueSummary(t *testing.T) {
issue := Issue{
AbsFilename: "/src/app/main.py",
Message: "undefined name",
Location: Location{Row: 12},
}
got := issue.Summary()
want := "app/main.py:12 error: undefined name"
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
}
Run them like any Go test (these don't require the engine):
go test ./...
Functional tests via checks​
For behavior that exercises containers and the Dagger API, write functions in your module and invoke them, or model them as checks so they run under dagger check. A check that builds, lints, or tests your project doubles as both a CI gate and a smoke test:
# Smoke test: does it build?
dagger call build
# Run all checks
dagger check
# Run generators and confirm there's no drift
dagger check --generate
In CI​
Run dagger check in CI to execute every check the module exposes. Because the heavy lifting happens in content-addressed containers, the same command runs identically on a laptop and on a CI runner, with full caching:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v6
with:
verb: check
IDE setup, go.mod and go.work​
init generates a go.mod (and go.sum) scoped to your module so the generated client resolves. If the module lives in a sub-directory of a larger Go project, this separate module can confuse IDE tooling.
Two options:
-
Reuse a parent
go.mod. If a parent directory already has ago.modyou want to use, delete the module's generatedgo.mod/go.sum; Dagger will fall back to the parent. Avoid this if your module pulls in dependencies irrelevant to the rest of the project, since most consumers prefer a narrower dependency set. -
Use a Go workspace. Keep the generated
go.modand stitch everything together withgo.work:# in the root of your repository
go work init
go work use ./
go work use ./path/to/moduleRestart your IDE and features like go-to-definition will work across both modules.
go.work should usually stay out of version control:
echo go.work >> .gitignore
echo go.work.sum >> .gitignore
Source maps​
The Go SDK attaches source maps to type and function definitions — ./path/to/file:line annotations recorded during generation. Most IDEs can follow these (natively or via a plugin such as the Open file plugin for VS Code), so you can click straight from a Dagger Function in one module to its declaration in another.
Packaging and release​
A Go SDK module is distributed as a Git repository. There is no build artifact to publish — consumers fetch the source by reference.
- Commit the generated files. Keeping
dagger.gen.goandinternal/in version control (the default) means consumers can use your module without running codegen first. - Version with Git tags. Releases are Git refs. Tag a release (e.g.
v1.2.0) and consumers pin it with@v1.2.0in the reference. - Pin a sensible engine version. Use
mod engine requireso consumers know which engine your module targets.
Consumers add your module as a dependency with the user-facing workspace command:
dagger mod install github.com/you/your-module@v1.2.0
or by adding it to their dagger.json dependencies and regenerating. A module reference follows [proto://]host/repo[/subpath][@version]; the version may be a tag, branch, or commit, and is resolved over HTTPS or SSH depending on available authentication.
Troubleshooting​
dagger init / dagger develop not found. These commands were removed. Use the Go SDK module: dagger -m github.com/dagger/go-sdk call init ... to scaffold and ... call mod generate ... to regenerate.
Nothing was written after init. init returns a changeset; it doesn't write files on its own. Apply it directly (review, or pass -y to apply without prompting).
A new function or dependency doesn't show up. Regenerate bindings with dagger -m github.com/dagger/go-sdk call mod generate (or dagger generate). The API surface comes from the generated internal/dagger package, which must be in sync with your code and dagger.json.
mod generate returns an empty changeset. A "skip generate" marker is likely in effect for this module or an ancestor. Confirm with dagger -m github.com/dagger/go-sdk call mod skip-generate.
Build/import errors referencing internal/dagger or dag. The generated client is stale or missing. Regenerate as above; if go.mod/go.sum drifted, ensure they match the regenerated code (a workspace go.work can help your IDE here).
git diff is noisy after generation. Confirm .gitattributes marks the generated paths as linguist-generated. If you'd rather not track them at all, re-init with --ignore-generated, or set codegen.automaticGitignore in dagger.json.
Engine version mismatch. Align the module with mod engine require --version <v> (or require-current / require-latest), then regenerate.
Next steps​
- Module Developer Guide — platform concepts that apply to every SDK
- Type system
- Checks
- Generating Code and Changesets