Build a CI Pipeline with Dagger and Go

Write a Go-based Dagger module that builds, tests, and publishes a container.

Introduction

30 Second Summary

CI pipelines are the backbone of modern software delivery. Most teams define them in YAML files that are difficult to test, impossible to debug locally, and silently break when someone edits the wrong line.

In this project, you will build a complete CI pipeline using Go code and Dagger's SDK. Your pipeline will build a Vue application, run its tests, package it into a production container with NGINX, and publish it to a container registry.

What You'll Build

You run a single command in your terminal and watch a real-time trace as your pipeline builds an app, tests it, packages it into a container, and pushes it to a registry.

By the end of this project, you'll have:

  • A Go-based Dagger module with four functions that compose into a full CI pipeline you can run locally with one command.
  • Hands-on experience with Dagger Shell and interactive container terminals for debugging build environments from the inside.
  • First-hand proof of content-addressed caching by watching a second pipeline run complete in seconds because nothing changed.
  • Secret Mission: Add a validation check function and run dagger check to see how Dagger's module extension system works.

Are there any prerequisites?

This project assumes you have basic Go programming knowledge, familiarity with Git and GitHub, and Docker installed and running on your machine. You do not need any JavaScript or Vue experience.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. You're going to write a complete CI pipeline in Go code using Dagger's SDK, giving you a contributor's understanding of how Dagger's Engine, modules, and functions work together.

Install Dagger CLI and Clone the Example App

Every Dagger pipeline starts with two pieces: the Dagger CLI (the command-line client that talks to the Dagger Engine) and a real application to build a pipeline for.

In this step, you will install the CLI pinned to version 0.21.4 and clone a ready-made Vue app that already has unit tests configured. This gives you a concrete build target for the Go pipeline you will write next.

In this step, get ready to:
  • Install the Dagger CLI (v0.21.4).
  • Clone the hello-dagger-template repository from GitHub.
  • Verify both tools are working.
Install the Dagger CLI

The Dagger CLI is the binary you will use to invoke every pipeline operation. When you run a command, the CLI connects to the Dagger Engine (a container running in Docker), which executes your pipeline steps inside isolated containers.

  • Check whether Dagger is already installed by running this command:
dagger version

✔️ I see version 0.21.4 or higher

You already have the correct version installed. Skip ahead to the next substep.

ⓧ I see an older version

You have an outdated Dagger CLI. Reinstall the pinned version by running this command:

curl -fsSL https://dl.dagger.io/dagger/install.sh | DAGGER_VERSION=0.21.4 BIN_DIR=/usr/local/bin sh

What does this command do?

  • Downloads the official Dagger install script from dl.dagger.io.
  • DAGGER_VERSION=0.21.4 pins the exact release so everyone on the project uses the same build.
  • BIN_DIR=/usr/local/bin tells the script where to place the binary so it is available system-wide.
  • Confirm the upgrade by running:
dagger version

You should see output starting with dagger v0.21.4.

ⓧ Command not found

Dagger is not installed yet. Install it by running this command:

curl -fsSL https://dl.dagger.io/dagger/install.sh | DAGGER_VERSION=0.21.4 BIN_DIR=/usr/local/bin sh

What does this command do?

  • Downloads the official Dagger install script from dl.dagger.io.
  • DAGGER_VERSION=0.21.4 pins the exact release so everyone on the project uses the same build.
  • BIN_DIR=/usr/local/bin tells the script where to place the binary so it is available system-wide.
  • Confirm the installation succeeded by running:
dagger version

You should see output starting with dagger v0.21.4.

Permission denied?

If the install script fails with a permission error when writing to /usr/local/bin, your user may not have write access to that directory. Prefix the command with sudo and try again.

Also confirm that Docker is running. The Dagger Engine needs Docker to start its container.

Help me fix this Dagger installation error

Clone the example app

The dagger/hello-dagger-template repository is a Vue 3 + Vite skeleton app with unit tests already configured using Vitest. You do not need to know Vue or JavaScript for this project. The app is simply the build target for your Dagger pipeline.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone the repository by running this command:
git clone https://github.com/dagger/hello-dagger-template.git

You will see Git output confirming the clone into a folder called hello-dagger-template.

  • Move into the project and list its contents by running these commands:
cd hello-dagger-template
ls

You should see package.json, src/, vitest.config.ts, and several other files in the listing.

Don't see the repository?

Make sure you ran cd ~/Desktop before cloning. If git clone fails with a network error, check your internet connection and try again.

Help me troubleshoot this clone error

Verify both are working

Before you run the next command, predict what you will see. You installed Dagger v0.21.4 and cloned the template app. What output do you expect from dagger version?

  • Confirm the Dagger CLI is accessible from inside the project directory by running:
dagger version

You should see:

dagger v0.21.4 (registry.dagger.io/engine:v0.21.4) linux/amd64
  • Confirm the project files are present by running:
ls

You should see package.json, src/, and vitest.config.ts in the output. These are the files your Dagger pipeline will build and test.

What is this app?

The hello-dagger-template is a minimal Vue app that displays a "Hello from Dagger!" page. It uses Vite as its build tool and Vitest for unit tests.

Your pipeline will install its dependencies, run those tests, build production files, and package everything into an NGINX container.

Your environment is ready. Next up, you will initialize a Dagger module and write your first function that creates a containerized build environment.

Initialize a Dagger Module and Write Your First Function

You have the Dagger CLI installed and a real application ready to build a pipeline for. Now you need a way to extend the Dagger API with your own custom functions.

A Dagger module is the mechanism for this. Initializing a module creates Go scaffolding and connects your code to the Dagger Engine. In this step, you'll write a minimal function to see the full round trip: Go code to Engine to container to output.

In this step, get ready to:
  • Initialize a Dagger module with the Go SDK.
  • Write a BuildEnv function that creates a Node.js development container.
  • Call the function and see the Dagger Engine execute it inside a container.
Initialize the Dagger module

Without a module, the Dagger Engine doesn't know about your custom functions. The dagger init command creates the module scaffolding and registers it with the Engine.

  • Initialize a new Dagger module using the Go SDK by running this command:
dagger init --sdk=go --name=hello-dagger

What does this command do?

This tells the Dagger CLI to create a new module named hello-dagger using the Go SDK.

The --sdk=go flag tells Dagger to generate Go bindings so you can write your pipeline functions in Go. The --name=hello-dagger flag sets the module name, which determines the Go import path.

The first time you run a Dagger command, it auto-provisions the Dagger Engine as a Docker container. This may take a moment while it pulls the engine image.

  • Confirm the generated files by running:
ls dagger.json .dagger/

You should see dagger.json at the project root, and inside .dagger/ you should see main.go, go.mod, go.sum, dagger.gen.go, and an internal/ directory.

What are all these files?

  • dagger.json is the module configuration file that tells the Engine this directory is a Dagger module.
  • .dagger/main.go is where your Dagger Function code lives.
  • .dagger/go.mod and go.sum manage Go dependencies for the module.
  • .dagger/dagger.gen.go and internal/ are auto-generated type bindings and helpers. You never edit these files directly.

Seeing a Docker error?

The Dagger Engine runs as a Docker container. Make sure Docker is running on your machine before running any dagger commands.

If you see a connection error, start Docker Desktop (or the Docker daemon) and try again. Help me fix a Docker connection error with Dagger

Write the BuildEnv function

The generated .dagger/main.go contains sample code. You'll replace it with a BuildEnv function that creates a complete Node.js development environment inside a container.

  • Open .dagger/main.go in your text editor and replace all of its contents with the following code:
package main

import (
	"context"
	"fmt"
	"math"
	"math/rand"

	"dagger/hello-dagger/internal/dagger"
)

type HelloDagger struct{}

// Build a ready-to-use development environment
func (m *HelloDagger) BuildEnv(
	// +defaultPath="/"
	source *dagger.Directory,
) *dagger.Container {
	nodeCache := dag.CacheVolume("node")
	return dag.Container().
		From("node:21-slim").
		WithDirectory("/src", source).
		WithMountedCache("/root/.npm", nodeCache).
		WithWorkdir("/src").
		WithExec([]string{"npm", "install"})
}

What does this code do?

  • HelloDagger is the main module struct. Every method on this struct becomes a Dagger Function that the Engine can call.
  • BuildEnv creates a Node.js container from node:21-slim, mounts your project source code into /src, and runs npm install to install dependencies.
  • The dag variable is a pre-initialized Dagger API client available in every Dagger Function. It provides access to core types like Container and Directory.
  • dag.CacheVolume("node") creates a persistent cache volume for npm packages. This means subsequent runs skip re-downloading dependencies.
  • The // +defaultPath="/" annotation makes the source parameter default to the module's context directory (your project root).

The extra imports (context, fmt, math, math/rand) are not used yet. You'll need them for functions you add in later steps. Dagger's build system handles these without error.

  • Save .dagger/main.go.
Call the function and verify it works

Your Go function BuildEnv becomes build-env in Dagger Shell (Go PascalCase converts to kebab-case). You'll call it now and watch the Dagger TUI trace every container operation in real time.

  • Run your function by executing this command:
dagger -c 'build-env'

The Dagger TUI appears, showing each operation as it executes: pulling the node:21-slim image, mounting source, and running npm install. This is the Dagger Engine executing your Go function inside containers.

Before you run the verification command, what do you think will happen when you ask for npm --version inside the container that BuildEnv created?

  • Verify the container is configured correctly by running:
dagger -c 'build-env | with-exec npm --version | stdout'

You should see a Node.js package manager version number printed (such as 10.x.x). This proves your container was created with Node installed and dependencies cached.

What is this piping syntax?

Dagger Shell uses | (pipe) to chain function calls together. Each segment calls a method on the result of the previous one.

So build-env | with-exec npm --version | stdout means: call BuildEnv to get a container, then run npm --version inside it, then capture its standard output as a string.

Not seeing a version number?

If the command fails with a module error, make sure you saved .dagger/main.go and that you're running the command from the project root (where dagger.json lives).

If you see a timeout or network error, the Engine is pulling the node:21-slim image for the first time. Wait a moment and try again.

Help me debug why my Dagger build-env function isn't returning output

✔️ Awesome, I've got everything!

Great. Double check that you've saved .dagger/main.go and move on to the next step.

ⓧ I'd like to double check the full code

Here is the complete .dagger/main.go at this point:

package main

import (
	"context"
	"fmt"
	"math"
	"math/rand"

	"dagger/hello-dagger/internal/dagger"
)

type HelloDagger struct{}

// Build a ready-to-use development environment
func (m *HelloDagger) BuildEnv(
	// +defaultPath="/"
	source *dagger.Directory,
) *dagger.Container {
	nodeCache := dag.CacheVolume("node")
	return dag.Container().
		From("node:21-slim").
		WithDirectory("/src", source).
		WithMountedCache("/root/.npm", nodeCache).
		WithWorkdir("/src").
		WithExec([]string{"npm", "install"})
}

Your first Dagger Function is alive and running inside the Engine. Next up, you'll add test and build functions that chain on top of BuildEnv to form a real CI pipeline.

Add Test and Build Functions

Your Dagger module can create a build environment inside a container. That's a solid foundation, but a CI pipeline needs to do more than just install dependencies.

A real pipeline runs tests against your code and produces a deployable artifact. In this step, you'll write two new Dagger Functions that chain on top of BuildEnv to run the app's unit tests and package it into a production-ready NGINX container.

In this step, get ready to:
  • Write a Test function that runs unit tests inside the build environment.
  • Write a Build function that produces a production NGINX container.
  • Serve the built container locally and verify it works.
Add the Test function

The first thing any CI pipeline does after setting up the environment is run tests. Your Test function will reuse the container that BuildEnv creates, then execute the app's Vitest suite inside it.

  • In .dagger/main.go, add the following Test function above your existing BuildEnv function:
// Return the result of running unit tests
func (m *HelloDagger) Test(
	ctx context.Context,
	// +defaultPath="/"
	source *dagger.Directory,
) (string, error) {
	return m.BuildEnv(source).
		WithExec([]string{"npm", "run", "test:unit", "run"}).
		Stdout(ctx)
}

What does this code do?

  • m.BuildEnv(source) reuses the Node.js container with dependencies already installed. No duplicate work.
  • WithExec runs npm run test:unit run inside that container, executing the Vitest suite.
  • Stdout(ctx) captures the test output as a string and returns it. The ctx parameter signals to the Dagger Engine that this function actually executes (rather than building a lazy pipeline).
  • Save .dagger/main.go.
  • Run the tests by calling your new function:
dagger -c 'test'

The Dagger TUI traces each operation in real time. You should see the test suite output showing passing tests at the end.

Seeing a compilation error?

  • Make sure the Test function is placed outside the BuildEnv function, not nested inside it. Each function is a separate method on the HelloDagger struct.
  • Check that your import block still includes "context" since the Test function uses context.Context.
  • help me debug a compilation error in my Dagger module
Add the Build function and serve it locally

Tests pass. Now you need a function that produces a production container. The Build function performs a multi-stage build: it compiles the Vue app, extracts the built files, then places them into a lightweight NGINX container.

  • In .dagger/main.go, add the following Build function above your Test function:
// Build the application container
func (m *HelloDagger) Build(
	// +defaultPath="/"
	source *dagger.Directory,
) *dagger.Container {
	build := m.BuildEnv(source).
		WithExec([]string{"npm", "run", "build"}).
		Directory("./dist")
	return dag.Container().From("nginx:1.25-alpine").
		WithDirectory("/usr/share/nginx/html", build).
		WithExposedPort(80)
}

What does this code do?

  • m.BuildEnv(source) reuses the same build environment. Then WithExec runs npm run build to compile the Vue app.
  • Directory("./dist") extracts just the built output files from the container's filesystem.
  • The second half creates a fresh nginx:1.25-alpine container, copies the built files into NGINX's default serve directory, and exposes port 80. This is a multi-stage build expressed as function composition.
  • Save .dagger/main.go.

Before you run the next command, predict what you'll see. The Build function creates an NGINX container serving the compiled Vue app on port 80. If you map that to port 8080 on your machine, what should appear at http://localhost:8080?

  • Start the built container as a local service by running:
dagger -c 'build | as-service | up --ports=8080:80'
  • Open http://localhost:8080 in your browser.

You should see a page that says "Hello from Dagger!" served by the NGINX container your Build function created. The TUI shows the full pipeline trace with each function's execution time.

What does this command do?

  • build calls your Build function and returns a container.
  • as-service converts that container into a running service.
  • up --ports=8080:80 starts the service and maps port 80 inside the container to port 8080 on your machine.
  • Press Ctrl+C in your terminal to stop the service.

Not seeing the page at localhost:8080?

  • Make sure the command is still running in your terminal. The service stops as soon as you press Ctrl+C or close the terminal.
  • Check that no other process is already using port 8080. Try lsof -i :8080 to see what's occupying the port.
  • Verify your Build function has WithExposedPort(80) at the end of the return statement.
  • help me debug why localhost:8080 isn't showing my Dagger-built app

✔️ Awesome, I've got everything!

Great. Make sure you've saved .dagger/main.go with both new functions in place.

ⓧ I'd like to double check the full code

Confirm your .dagger/main.go contains these sections in order (after the import block and type HelloDagger struct{}):

  • The Build function (creates the NGINX production container with built files).
  • The Test function (runs npm run test:unit run and returns stdout).
  • The BuildEnv function (creates the Node.js container with dependencies cached).

The import block should still have "context", "fmt", "math", "math/rand", and "dagger/hello-dagger/internal/dagger". Scroll up to compare your code against the chunks above.

You now have two functions that compose on top of BuildEnv. Next, you'll add a Publish function that ties everything together and pushes the container to a registry. You'll also experience Dagger's caching first-hand.

Publish the Container and Experience Caching

Publishing to a container registry is the final step of any CI pipeline. Your Publish function will tie everything together by running tests, building the container, and pushing it to a registry in one call.

More importantly, re-running the pipeline shows Dagger's content-addressed caching in action. The first run takes time. The second completes in seconds because nothing changed. This architectural insight is what makes Dagger fast.

In this step, get ready to:
  • Add a Publish function that runs tests, builds, and publishes to a temporary registry.
  • Experience Dagger's content-addressed caching by running the pipeline twice.
  • Pull and run the published image locally with Docker.
Add the Publish function

Your pipeline needs a single entry point that orchestrates the entire flow: test, build, and publish. The Publish function calls Test first (so broken code never ships), then calls Build, then pushes the resulting container to ttl.sh.

  • In .dagger/main.go, find the line that says type HelloDagger struct{}.
  • Add the following Publish function directly below it, above the existing Build function:
// Publish the application container after building and testing it on-the-fly
func (m *HelloDagger) Publish(
	ctx context.Context,
	// +defaultPath="/"
	source *dagger.Directory,
) (string, error) {
	_, err := m.Test(ctx, source)
	if err != nil {
		return "", err
	}
	return m.Build(source).
		Publish(ctx, fmt.Sprintf("ttl.sh/hello-dagger-%.0f", math.Floor(rand.Float64()*10000000))) //#nosec
}

What does this code do?

  • m.Test(ctx, source) runs unit tests first. If any test fails, err is non-nil and the function returns early. Broken code never reaches the registry.
  • m.Build(source) produces the NGINX container with the built Vue app inside.
  • .Publish(ctx, ...) pushes that container image to a registry URL. The fmt.Sprintf generates a random name like ttl.sh/hello-dagger-4829103.

This is function composition. Each function builds on the one below it, creating a pipeline through plain Go method calls.

What is ttl.sh?

ttl.sh is a free, anonymous container registry that requires no account or authentication. Images are automatically deleted after their time-to-live expires (default is 1 hour). It is perfect for testing CI pipelines without setting up credentials.

  • Save .dagger/main.go.
  • Run your full pipeline by executing this command:
dagger -c 'publish'

The Dagger TUI traces every step in real time: installing dependencies, running tests, building the app, packaging into NGINX, and pushing to the registry. When it finishes, the final line prints the published image URL.

Seeing a compilation error?

  • Make sure the Publish function is placed after type HelloDagger struct{} and before the Build function. Order matters for readability but not compilation, so check for typos in the function signature.
  • Verify that ctx context.Context is the first parameter. The Dagger SDK requires context as the first argument for functions that perform I/O.
  • Check that the imports at the top of the file include "context", "fmt", "math", and "math/rand". These should already be there from the previous step.

Why is my publish function failing?

Experience content-addressed caching

Dagger's engine tracks every input to every operation. If nothing changed (no new source files, no different commands), the cached result is reused instantly. This is the same architectural pattern that makes tools like Nix and Bazel fast.

  • Before you run the pipeline again, predict: how long do you think the second run will take compared to the first? Will it re-install npm packages and re-run tests?
  • Run the pipeline a second time:
dagger -c 'publish'

The entire pipeline completes in seconds. Watch the TUI. Every operation shows CACHED next to it because no source files changed between runs.

Why is the second run so fast?

Dagger uses content-addressed caching. Every container operation is keyed by its inputs: the base image digest, the mounted files, and the exact command. If all inputs are identical, the engine skips execution and returns the cached output.

This is different from layer caching in a Dockerfile. Dagger caches at the operation level, so even a function that chains five operations can skip all of them independently. Change one source file and only the operations downstream of that change re-execute.

Pull and run the published image

The published image is a real container sitting in a public registry. You can pull and run it with Docker just like any other image.

  • Copy the ttl.sh/hello-dagger-... URL that was printed at the end of your dagger -c 'publish' output.
  • Run the published image locally by executing this command (replacing the URL with yours):
docker run -p 8080:80 [[PUBLISHED_URL="your ttl.sh/hello-dagger-XXXXXXX URL from the output above"]]
  • Open http://localhost:8080 in your browser.

You should see the same "Hello from Dagger!" page served by NGINX. This is the exact container your pipeline built and published.

  • Press Ctrl+C in your terminal to stop the container.

Port already in use?

  • If you left the container from Step 3 running, port 8080 is still occupied. Stop it with Ctrl+C in that terminal, then retry the docker run command.
  • If the ttl.sh image has expired (images last 1 hour by default), run dagger -c 'publish' again to push a fresh one.

Help me debug why docker run is not serving my published Dagger container on localhost:8080.

✔️ Awesome, I've got everything!

Great. Double check you have saved .dagger/main.go with the Publish function in place.

ⓧ I'd like to double check the full code

Your .dagger/main.go should contain these functions in this order. Scroll through your file and confirm each one is present:

  • Package declaration and imports (context, fmt, math, math/rand, dagger/hello-dagger/internal/dagger).
  • type HelloDagger struct{} struct declaration.
  • Publish function: calls Test, then Build, then publishes to ttl.sh. Returns (string, error).
  • Build function: calls BuildEnv, runs npm run build, extracts ./dist, returns NGINX container.
  • Test function: calls BuildEnv, runs npm run test:unit run, returns stdout.
  • BuildEnv function: creates node:21-slim container with source mounted, npm cache, and npm install run.

Your pipeline is fully operational. You can build, test, and publish a containerized app with a single command. Next up, you will explore Dagger Shell and interactive debugging to understand how to inspect what is happening inside the engine.

Explore with Dagger Shell and Interactive Debugging

You now have a complete CI pipeline that builds, tests, publishes, and leverages caching. As a future contributor to Dagger, the next skill you need is the ability to inspect what the Dagger Engine is doing at any point in a pipeline.

Dagger Shell is an interactive REPL that lets you call API functions, chain them together, and drop into running containers. This is the same tool you would use to debug issues when working on the Dagger codebase itself.

In this step, get ready to:
  • Launch Dagger Shell and explore the API interactively with .help.
  • Drop into a running build container and inspect its filesystem.
  • List all the functions your module exposes with dagger functions.
Launch Dagger Shell and explore the API

Every command you have run so far used dagger -c for one-shot execution. Running dagger with no arguments opens an interactive session where you can build up pipelines step by step and inspect intermediate results.

  • Launch Dagger Shell by running:
dagger

What happens when you run this?

The CLI connects to the Dagger Engine and drops you into an interactive prompt. It loads your current module automatically, so your custom functions are available alongside the core Dagger API.

You should see the Dagger Shell prompt appear, ready for commands.

  • Type .help and press Enter to see the available types and functions.
.help

What does .help show?

The .help command lists all the types and functions available in the current context. You will see core Dagger types like Container and Directory, plus your custom module functions (build-env, test, build, publish).

You can also append .help to any in-progress pipeline for context-sensitive help on what you can do next.

Now try chaining core API calls together to see how pipelines work interactively.

  • Run this pipeline to pull an Alpine container and read its OS info:
container | from alpine | with-exec cat /etc/os-release | stdout

You should see the contents of Alpine's /etc/os-release file printed, confirming that function chaining works interactively just as it does in one-shot mode.

How does function chaining work?

Each segment separated by | pipes the result of the previous function into the next. container creates an empty container object. from alpine sets its base image. with-exec runs a command inside it. stdout extracts the output as text.

This is the same pattern your Go functions use. The BuildEnv function chains From, WithDirectory, WithMountedCache, and WithExec in exactly the same way.

Not seeing any output?

Make sure you typed the full pipeline on one line. Each | must have a space on both sides. If you see an error about cat, ensure the command is with-exec cat /etc/os-release with no extra quotes.

help me debug why my Dagger Shell pipeline isn't returning output

Drop into your build environment with an interactive terminal

One of the most powerful debugging features is the ability to open a live terminal session inside any container your pipeline creates. You can inspect files, run commands, and verify state without leaving the Dagger session.

  • Still inside Dagger Shell, run this to open a bash session inside your build environment container:
build-env | terminal --cmd=bash

What is happening here?

This calls your build-env function (which creates the Node.js container with your source code and dependencies installed), then opens an interactive bash terminal inside that running container.

You are now inside the same container your CI pipeline uses to run tests and builds. Everything you inspect here reflects the exact state your other functions see.

Your terminal prompt changes to show you are inside the container.

  • Verify that dependencies are installed by running:
ls node_modules/ | head -5

You should see package directory names listed, confirming npm install ran successfully in the container.

  • Check that your source code is mounted by running:
ls /src/

You should see your project files including package.json, src/, and vitest.config.ts.

  • Exit the container terminal by typing:
exit

You are now back in Dagger Shell.

  • Exit Dagger Shell by pressing Ctrl+D or typing exit.

You are now back in your regular terminal.

Terminal didn't open?

If you see an error when running build-env | terminal --cmd=bash, make sure Docker is still running. The Dagger Engine needs Docker to create and manage containers.

help me troubleshoot why the Dagger interactive terminal won't open

List your module's functions

The Dagger CLI can introspect any module and list the functions it exposes. This is the same mechanism other modules use when they depend on yours. It is also how the CLI knows what commands are available when you type .help in Dagger Shell.

Before you run the next command, think about what you expect to see. How many functions did you define in your module, and what are their kebab-case names?

  • List all functions your module exposes by running:
dagger functions

You should see your four functions listed: build-env, test, build, and publish. Each one shows its description (from the Go doc comment) and its return type.

Why kebab-case?

Go function names like BuildEnv are automatically converted to build-env in the CLI. This convention makes function names shell-friendly while keeping your Go code idiomatic.

The same introspection powers the Dagger Shell's tab completion and the .help output you saw earlier.

Seeing fewer than four functions?

Make sure your .dagger/main.go file contains all four exported methods: BuildEnv, Test, Build, and Publish. Only exported methods (capitalized names) on the HelloDagger struct become Dagger Functions.

help me figure out why dagger functions isn't showing all my functions

Secret mission

Add a Validation Check and Extend the Dagger API

Your pipeline already builds, tests, and publishes. But what if you could add a validation gate that other modules can discover and run automatically? Extend the Dagger API with a check function that catches broken builds before they ship.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with no paid services, so there are no ongoing costs.

Resources you used:

  • A Dagger Engine container running in Docker (no ongoing cost, uses local resources only).
  • The hello-dagger-template project directory on your Desktop.
  • The Dagger CLI binary installed at /usr/local/bin/dagger.
  • A temporary container image on ttl.sh (auto-deleted after 24 hours, no account needed).

Keep everything running

No action needed. Choose this if you plan to keep exploring Dagger or want to build more pipeline functions.

  • The Dagger Engine container stays running in Docker and caches persist. There is no cost.
  • Your ttl.sh image will automatically disappear after 24 hours.
  • The Dagger CLI remains available for future projects.

Pause - I'll come back to this later

Stop Docker to free up memory and CPU. Your project files and caches stay on disk so you can pick up where you left off.

  • Stop Docker Desktop (or the Docker daemon if you run it as a service). The Dagger Engine container stops automatically.
  • When you're ready to return, start Docker again. The Dagger Engine will re-provision itself on the next dagger command.
  • Caches persist on disk, so your next pipeline run will still benefit from previously cached layers.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to fully clean up.

  • Remove the Dagger Engine container and its volumes by running:
  • Remove the project directory by running:
  • Remove the Dagger cache and config directories by running:
  • Uninstall the Dagger CLI by running:
  • Your ttl.sh image requires no action. It auto-deletes after 24 hours.

Nice Work!

Nice Work!

You just built a complete CI pipeline entirely in Go code using Dagger. No YAML, no shell scripts, no vendor lock-in.

You've learned how to:

  • Write four composable Dagger Functions in Go (BuildEnv, Test, Build, Publish) that chain together into a full CI pipeline building, testing, and publishing a containerized application.
  • Experience content-addressed caching first-hand by watching a second pipeline run complete in seconds because nothing changed.
  • Use Dagger Shell and interactive terminals to explore running containers, chain API calls, and debug your pipeline from the inside.
  • Secret Mission: Add a ValidateBuild check function and run dagger check to see how Dagger's module extension system dynamically extends the API.

Ready to quiz yourself?