Standardize Your Application's CI Functions as a Dagger Module
Introduction
Dagger lets you encapsulate all your project's CI tasks and workflows into Dagger Functions, written in your programming language of choice, and then call those functions, either locally from your development environment or remotely on your CI provider.
This gives teams standard, consistent tooling with reduced host environment requirements; you only need the Dagger CLI and the ability to run containers (no local dependencies like Golang, Python, Node, etc). Local and remote CI environments achieve parity; developers can run CI test, build and deployment pipelines locally and see the results almost instantly, and there are fewer surprises when pushing final code. A standard, cross-language toolkit enables new team members to become productive faster and reduce friction in cross-team collaboration.
This guide walks you through the process of creating a Dagger Module and Dagger Functions encapsulating common CI tasks for an application: testing, building, and publishing it. You will learn how to:
- Initialize a new Dagger Module as part of your application codebase
- Import modules from the Daggerverse to benefit from pre-packaged functionality
- Connect imported modules with your own Dagger Functions
- Call Dagger Functions to test, build, publish and run your application locally
- Understand how to work with containers as function input arguments and return values
Requirements
This guide assumes that:
- You know the basics of calling and writing Dagger Modules. If not, refer to the quickstart.
- You have the Dagger CLI installed in your development environment. If not, install the Dagger CLI.
- You have Docker installed and running on the host system. If not, install Docker.
- You have a Node.js Web application. If not, follow the steps in Appendix A to create an example Node.js application.
Step 1: Initialize a new module
The example module used in this guide builds, tests and publishes a Node.js application.
Run dagger init
in the application directory to bootstrap a new module:
- Go
- Python
- TypeScript
dagger init --name=my-module --sdk=go
This will generate a dagger.json
module file, an initial dagger/main.go
source file, as well as a dagger/dagger.gen.go
file and dagger/internal/
directory.
dagger init --name=my-module --sdk=python
This will generate a dagger.json
module file, initial dagger/src/main/__init__.py
, dagger/pyproject.toml
and dagger/requirements.lock
files, as well as a generated dagger/sdk
folder for local development.
dagger init --name=my-module --sdk=typescript
This will generate a dagger.json
module file, initial dagger/src/index.ts
, dagger/package.json
and dagger/tsconfig.json
files, as well as a generated dagger/sdk
folder for local development.
Step 2: Add a function to build the application base image
The first step is to add a function to build a base image containing the application source code and runtime. This base image will serve as an input to other functions.
Since the application is a Node.js application, it's convenient to use the node
module, which provides a set of ready-made functions to manage a Node.js project.
Dagger exposes every module using a language-agnostic GraphQL API. So, even though the node
module is written in TypeScript, you can transparently call its functions from your module written in Go, Python or any other supported language.
First, add the node
module as a dependency:
dagger install github.com/dagger/dagger/sdk/typescript/dev/node@9e59bae142f64975b7c9ad851e6bd4901d43513a
- Go
- Python
- TypeScript
Next, update the generated dagger/main.go
file with the following code:
package main
type MyModule struct{}
// build base image
func (m *MyModule) buildBaseImage(source *Directory) *Container {
return dag.Node(NodeOpts{Version: "21"}).
WithNpm().
WithSource(source).
Install(nil).
Container()
}
This function does the following:
- It calls the
node
module's constructor function via thedag
client. This function returns anode
container image with the given Node.js version. This container image is represented as aNode
object. - It calls the
Node.WithNpm()
function, which returns a revisedNode
object after adding thenpm
package manager and a cache volume fornpm
. - It calls the
Node.WithSource()
function, which returns a revisedNode
object including the application source code mounted in the container filesystem and a cache volume for Node.js modules.- The
Node.WithSource()
function accepts aDirectory
representing the application source code directory. This directory path will be passed as a command-line flag when calling the function using the CLI.
- The
- It calls the
Node.Install()
function, which runsnpm install
in the container and returns a revisedNode
object including the application's dependencies. - It calls the
Node.Container()
function, which returns aContainer
representing the final container image with the application source code, Node.js runtime and cache volume.
Next, update the generated dagger/src/main/__init__.py
file with the following code:
import dagger
from dagger import dag, object_type
@object_type
class MyModule:
def build_base_image(self, source: dagger.Directory) -> dagger.Container:
"""Build base image"""
return (
dag.node(version="21").with_npm().with_source(source).install().container()
)
This function does the following:
- It calls the
node
module's constructor function via thedag
client. This function returns anode
container image with the given Node.js version. This container image is represented as aNode
object. - It calls the
Node.with_npm()
function, which returns a revisedNode
object after adding thenpm
package manager and a cache volume fornpm
. - It calls the
Node.with_source()
function, which returns a revisedNode
object including the application source code mounted in the container filesystem and a cache volume for Node.js modules.- The
Node.with_source()
function accepts aDirectory
representing the application source code directory. This directory path will be passed as a command-line flag when calling the function using the CLI.
- The
- It calls the
Node.install()
function, which runsnpm install
in the container and returns a revisedNode
object including the application's dependencies. - It calls the
Node.container()
function, which returns aContainer
representing the final container image with the application source code, Node.js runtime and cache volume.
Next, update the generated dagger/src/index.ts
file with the following code:
import { dag, Container, Directory, object } from "@dagger.io/dagger"
@object()
class MyModule {
/*
* Build base image
*/
buildBaseImage(source: Directory): Container {
return dag
.node({ version: "21" })
.withNpm()
.withSource(source)
.install([])
.container()
}
}
This function does the following:
- It calls the
node
module's constructor function via thedag
client. This function returns anode
container image with the given Node.js version. This container image is represented as aNode
object. - It calls the
Node.withNpm()
function, which returns a revisedNode
object after adding thenpm
package manager and a cache volume fornpm
. - It calls the
Node.withSource()
function, which returns a revisedNode
object including the application source code mounted in the container filesystem and a cache volume for Node.js modules.- The
Node.withSource()
function accepts aDirectory
representing the application source code directory. This directory path will be passed as a command-line flag when calling the function using the CLI.
- The
- It calls the
Node.install()
function, which runsnpm install
in the container and returns a revisedNode
object including the application's dependencies. - It calls the
Node.container()
function, which returns aContainer
representing the final container image with the application source code, Node.js runtime and cache volume.
dag
is the Dagger client, which is pre-initialized. It contains all the core types (like Container
, Directory
, etc.), as well as bindings to any dependencies your module has declared (like node
).
Step 3: Add a function to test the application
The return value of the BuildBaseImage()
API is a Container
object with the application source code, Node.js runtime and cache volume. This is everything needed to test, build and publish the application.
Add a new function that runs tests for the example application, by executing the test:unit run
command:
- Go
- Python
- TypeScript
package main
import (
"context"
)
type MyModule struct{}
// run unit tests
func (m *MyModule) Test(ctx context.Context, source *Directory) (string, error) {
return dag.Node(NodeOpts{Ctr: m.buildBaseImage(source)}).
Commands().
Run([]string{"test:unit", "run"}).
Stdout(ctx)
}
// build base image
func (m *MyModule) buildBaseImage(source *Directory) *Container {
return dag.Node(NodeOpts{Version: "21"}).
WithNpm().
WithSource(source).
Install(nil).
Container()
}
This function does the following:
- It calls the
node
module's constructor function via thedag
client and passes it the container returned by theBuildBaseImage()
function. TheNode
constructor returns aNode
object. - It calls the
Node.Commands()
function, which returns a revisedContainer
configured with various pre-defined commands. - It calls the
Node.Run()
function, which returns a revisedNode
object after setting the commands to run in the container image - in this case, the commandnpm run test:unit run
. - It uses the
Container.Stdout()
function to return the output of the last executed command. If tests pass, the output shows the list of passed tests. If not, a non-nil error is returned, which propagates to the Dagger CLI and lets it know that one or more tests failed.
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def test(self, source: dagger.Directory) -> str:
"""Run unit tests"""
return await (
dag.node(ctr=self.build_base_image(source))
.commands()
.run(["test:unit", "run"])
.stdout()
)
def build_base_image(self, source: dagger.Directory) -> dagger.Container:
"""Build base image"""
return (
dag.node(version="21").with_npm().with_source(source).install().container()
)
This function does the following:
- It calls the
node
module's constructor function via thedag
client and passes it the container returned by thebuild_base_image()
function. TheNode
constructor returns aNode
object. - It calls the
Node.commands()
function, which returns a revisedContainer
configured with various pre-defined commands. - It calls the
Node.run()
function, which returns a revisedNode
object after setting the commands to run in the container image - in this case, the commandnpm run test:unit run
. - It uses the
Container.stdout()
function to return the output of the last executed command. If tests pass, the output shows the list of passed tests. If not, a non-nil error is returned, which propagates to the Dagger CLI and lets it know that one or more tests failed.
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/*
* Run unit tests
*/
@func()
async test(source: Directory): Promise<string> {
return await dag
.node({ ctr: this.buildBaseImage(source) })
.commands()
.run(["test:unit", "run"])
.stdout()
}
/*
* Build base image
*/
buildBaseImage(source: Directory): Container {
return dag
.node({ version: "21" })
.withNpm()
.withSource(source)
.install([])
.container()
}
}
This function does the following:
- It calls the
node
module's constructor function via thedag
client and passes it the container returned by thebuildBaseImage()
function. TheNode
constructor returns aNode
object. - It calls the
Node.commands()
function, which returns a revisedContainer
configured with various pre-defined commands. - It calls the
Node.run()
function, which returns a revisedContainer
object after setting the commands to run in the container image - in this case, the commandnpm run test:unit run
. - It uses the
Container.stdout()
function to return the output of the last executed command. If tests pass, the output shows the list of passed tests. If not, a non-nil error is returned, which propagates to the Dagger CLI and lets it know that one or more tests failed.
Try the function by running it as below:
dagger call test --source=.
For security, Dagger Modules do not have access to the host and so, host resources such as directories, files, environment variables, services and so on must be explicitly passed using command-line arguments. If your source directory is located somewhere other than the current working directory (i.e. .
), adjust the --source
argument value accordingly.
Here's an example of the output you will see:
> myapp@0.0.0 test:unit
> vitest run
RUN v1.1.0 /src
✓ src/components/__tests__/HelloWorld.spec.ts (1 test) 65ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 15:46:12
Duration 8.85s (transform 751ms, setup 0ms, collect 1.04s, tests 65ms, environment 4.51s, prepare 1.19s)
Step 4: Add a function to build the application
If your application passes all its tests, the typical next step is to build it.
Add a new function that creates a production build of the example application:
- Go
- Python
- TypeScript
package main
import (
"context"
)
type MyModule struct{}
// create a production build
func (m *MyModule) Build(source *Directory) *Directory {
return dag.Node(NodeOpts{Ctr: m.buildBaseImage(source)}).
Commands().
Build().
Directory("./dist")
}
// run unit tests
func (m *MyModule) Test(ctx context.Context, source *Directory) (string, error) {
return dag.Node(NodeOpts{Ctr: m.buildBaseImage(source)}).
Commands().
Run([]string{"test:unit", "run"}).
Stdout(ctx)
}
// build base image
func (m *MyModule) buildBaseImage(source *Directory) *Container {
return dag.Node(NodeOpts{Version: "21"}).
WithNpm().
WithSource(source).
Install(nil).
Container()
}
This function does the following:
- It calls the
node
module's constructor function via thedag
client and passes it the container returned by theBuildBaseImage()
function. TheNode
constructor returns aNode
object. - It calls the
Node.Commands()
function, which returns a revisedContainer
configured with various pre-defined commands. - It calls the
Node.Build()
function, which returns a revisedNode
object after setting thenpm run build
command to run in the container image. This command builds the application and places the build in adist/
directory in the container filesystem. - It obtains a reference to the
dist/
directory in the container with theContainer.Directory()
function. This function returns aDirectory
object.
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def build(self, source: dagger.Directory) -> dagger.Directory:
"""Create a production build"""
return (
dag.node(ctr=self.build_base_image(source))
.build()
.container()
.directory("./dist")
)
@function
async def test(self, source: dagger.Directory) -> str:
"""Run unit tests"""
return await (
dag.node(ctr=self.build_base_image(source))
.commands()
.run(["test:unit", "run"])
.stdout()
)
def build_base_image(self, source: dagger.Directory) -> dagger.Container:
"""Build base image"""
return (
dag.node(version="21").with_npm().with_source(source).install().container()
)
This function does the following:
- It calls the
node
module's constructor function via thedag
client and passes it the container returned by thebuild_base_image()
function. TheNode
constructor returns aNode
object. - It calls the
Node.commands()
function, which returns a revisedContainer
configured with various pre-defined commands. - It calls the
Node.build()
function, which returns a revisedNode
object after setting thenpm run build
command to run in the container image. This command builds the application and places the build in adist/
directory in the container filesystem. - It obtains a reference to the
dist/
directory in the container with theContainer.directory()
function. This function returns aDirectory
object.
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/*
* Create a production build
*/
@func()
build(source: Directory): Directory {
return dag
.node({ ctr: this.buildBaseImage(source) })
.commands()
.build()
.directory("./dist")
}
/*
* Run unit tests
*/
@func()
async test(source: Directory): Promise<string> {
return await dag
.node({ ctr: this.buildBaseImage(source) })
.commands()
.run(["test:unit", "run"])
.stdout()
}
/*
* Build base image
*/
buildBaseImage(source: Directory): Container {
return dag
.node({ version: "21" })
.withNpm()
.withSource(source)
.install([])
.container()
}
}
This function does the following:
- It calls the
node
module's constructor function via thedag
client and passes it the container returned by thebuildBaseImage()
function. TheNode
constructor function returns aNode
object. - It calls the
Node.commands()
functions, which returns a revisedContainer
configured with various pre-defined commands. - It calls the
Node.build()
function, which returns a revisedContainer
object after setting thenpm run build
command to run in the container image. This command builds the application and places the build in adist/
directory in the container filesystem. - It obtains a reference to the
dist/
directory in the container with theContainer.directory()
function. This function returns aDirectory
object.
The npm run build
command is appropriate for the example Vue application used in this guide, but other applications may use different commands. Modify your function code accordingly.
Try the function by running it as below. Note the additional chained call to Directory.Entries()
on the function's Directory
return value, to display a file listing for the build directory.
dagger call build --source=. entries
Here's an example of the output you will see:
assets
favicon.ico
index.html
If you'd like the directory to be exported to your local host, you can run the following command to export it to ./dist
.
dagger call build --source=. --output=./dist
The exported directory should now be available locally at ./dist
.
Step 5: Add a function to publish the application image
At this point, your Dagger Module has functions to test and build the application. However, the Dagger API and SDKs also have native support to publish container images to remote registries.
Update the module and add new functions to copy the built application into an NGINX web server container image and deliver the result to ttl.sh, an ephemeral Docker registry:
- Go
- Python
- TypeScript
package main
import (
"context"
"fmt"
"math"
"math/rand"
)
type MyModule struct{}
// publish an image
func (m *MyModule) Publish(ctx context.Context, source *Directory) (string, error) {
return m.Package(source).
Publish(ctx, fmt.Sprintf("ttl.sh/myapp-%.0f:10m", math.Floor(rand.Float64()*10000000))) //#nosec
}
// create a production image
func (m *MyModule) Package(source *Directory) *Container {
return dag.Container().From("nginx:1.25-alpine").
WithDirectory("/usr/share/nginx/html", m.Build(source)).
WithExposedPort(80)
}
// create a production build
func (m *MyModule) Build(source *Directory) *Directory {
return dag.Node(NodeOpts{Ctr: m.buildBaseImage(source)}).
Commands().
Build().
Directory("./dist")
}
// run unit tests
func (m *MyModule) Test(ctx context.Context, source *Directory) (string, error) {
return dag.Node(NodeOpts{Ctr: m.buildBaseImage(source)}).
Commands().
Run([]string{"test:unit", "run"}).
Stdout(ctx)
}
// build base image
func (m *MyModule) buildBaseImage(source *Directory) *Container {
return dag.Node(NodeOpts{Version: "21"}).
WithNpm().
WithSource(source).
Install(nil).
Container()
}
This code listing adds two functions:
- The
Package()
function calls theContainer.From()
function to initialize a new container from a base image - here, thenginx:1.25-alpine
image.TheFrom()
function returns a newContainer
object with the result.- It uses the
Container.WithDirectory()
function to write theDirectory
returned by theBuild()
function to the/usr/share/nginx/html
path in the container and return a revisedContainer
. - It uses the
Container.WithExposedPort()
function to expose port 80 (the default NGINX port in thenginx:1.25-alpine
image) and return a revisedContainer
.
- It uses the
- The
Publish()
function calls thePackage()
function to obtain the container image and then calls the built-inContainer.Publish()
function to publish it to the ttl.sh registry and return the image identifier.
import random
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def publish(self, source: dagger.Directory) -> str:
"""Publish an image"""
return await self.package(source).publish(
f"ttl.sh/myapp-{random.randrange(10 ** 8)}"
)
def package(self, source: dagger.Directory) -> dagger.Container:
"""Create a production image"""
return (
dag.container()
.from_("nginx:1.25-alpine")
.with_directory("/usr/share/nginx/html", self.build(source))
.with_exposed_port(80)
)
@function
def build(self, source: dagger.Directory) -> dagger.Directory:
"""Create a production build"""
return (
dag.node(ctr=self.build_base_image(source))
.build()
.container()
.directory("./dist")
)
@function
async def test(self, source: dagger.Directory) -> str:
"""Run unit tests"""
return await (
dag.node(ctr=self.build_base_image(source))
.commands()
.run(["test:unit", "run"])
.stdout()
)
def build_base_image(self, source: dagger.Directory) -> dagger.Container:
"""Build base image"""
return (
dag.node(version="21").with_npm().with_source(source).install().container()
)
This code listing adds two functions:
- The
package()
function calls theContainer.from_()
function to initialize a new container from a base image - here, thenginx:1.25-alpine
image.Thefrom_()
function returns a newContainer
object with the result.- It uses the
Container.with_directory()
function to write theDirectory
returned by thebuild()
function to the/usr/share/nginx/html
path in the container and return a revisedContainer
. - It uses the
Container.with_exposed_port()
function to expose port 80 (the default NGINX port in thenginx:1.25-alpine
image) and return a revisedContainer
.
- It uses the
- The
publish()
function calls thepackage()
function to obtain the container image and then calls the built-inContainer.publish()
method to publish it to the ttl.sh registry and return the image identifier.
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/*
* Publish an image
*/
@func()
async publish(source: Directory): Promise<string> {
return await this.package(source).publish(
"ttl.sh/myapp-" + Math.floor(Math.random() * 10000000),
)
}
/*
* Create a production image
*/
@func()
package(source: Directory): Container {
return dag
.container()
.from("nginx:1.25-alpine")
.withDirectory("/usr/share/nginx/html", this.build(source))
.withExposedPort(80)
}
/*
* Create a production build
*/
@func()
build(source: Directory): Directory {
return dag
.node({ ctr: this.buildBaseImage(source) })
.commands()
.build()
.directory("./dist")
}
/*
* Run unit tests
*/
@func()
async test(source: Directory): Promise<string> {
return await dag
.node({ ctr: this.buildBaseImage(source) })
.commands()
.run(["test:unit", "run"])
.stdout()
}
/*
* Build base image
*/
buildBaseImage(source: Directory): Container {
return dag
.node({ version: "21" })
.withNpm()
.withSource(source)
.install([])
.container()
}
}
This code listing adds two functions:
- The
package()
function calls theContainer.from()
function to initialize a new container from a base image - here, thenginx:1.25-alpine
image. TheContainer.from()
function returns a newContainer
object with the result.- It uses the
Container.withDirectory()
function to write theDirectory
returned by thebuild()
function to the/usr/share/nginx/html
path in the container and return a revisedContainer
. - It uses the
Container.withExposedPort()
function to expose port 80 (the default NGINX port in thenginx:1.25-alpine
image) and return a revisedContainer
.
- It uses the
- The
publish()
function calls thepackage()
function to obtain the container image and then calls the built-inContainer.publish()
function to publish it to the ttl.sh registry and return the image identifier.
Try the function by running the command below:
dagger call publish --source=.
Here's an example of the output you will see:
ttl.sh/myapp-6263158:10m@sha256:802f4edeb30b47b5ab4c52d8cccd9d18dd9f4c0d6a0a6b8015926d0290312bb0
Step 6: Add a function to run the application as a local service
Dagger Functions can return services as well as containers. These services can then be started in your local environment and have any exposed ports forwarded to the host machine. This has many potential use cases, such as manually testing web applications or database services directly from the host browser or host system.
In order for this to work, the container image used by the service must have one or more exposed ports defined. This is already implemented in the functions shown in the previous section. So, update the module and add a new function to return the built container image as a service:
- Go
- Python
- TypeScript
package main
import (
"context"
"fmt"
"math"
"math/rand"
)
type MyModule struct{}
// create a service from the production image
func (m *MyModule) Serve(source *Directory) *Service {
return m.Package(source).AsService()
}
// publish an image
func (m *MyModule) Publish(ctx context.Context, source *Directory) (string, error) {
return m.Package(source).
Publish(ctx, fmt.Sprintf("ttl.sh/myapp-%.0f:10m", math.Floor(rand.Float64()*10000000))) //#nosec
}
// create a production image
func (m *MyModule) Package(source *Directory) *Container {
return dag.Container().From("nginx:1.25-alpine").
WithDirectory("/usr/share/nginx/html", m.Build(source)).
WithExposedPort(80)
}
// create a production build
func (m *MyModule) Build(source *Directory) *Directory {
return dag.Node(NodeOpts{Ctr: m.buildBaseImage(source)}).
Commands().
Build().
Directory("./dist")
}
// run unit tests
func (m *MyModule) Test(ctx context.Context, source *Directory) (string, error) {
return dag.Node(NodeOpts{Ctr: m.buildBaseImage(source)}).
Commands().
Run([]string{"test:unit", "run"}).
Stdout(ctx)
}
// build base image
func (m *MyModule) buildBaseImage(source *Directory) *Container {
return dag.Node(NodeOpts{Version: "21"}).
WithNpm().
WithSource(source).
Install(nil).
Container()
}
This function simply calls the Package()
function created earlier to obtain the container image and then returns it as a service using the Container.AsService()
function.
import random
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def serve(self, source: dagger.Directory) -> dagger.Service:
"""Create a service from the production image"""
return self.package(source).as_service()
@function
async def publish(self, source: dagger.Directory) -> str:
"""Publish an image"""
return await self.package(source).publish(
f"ttl.sh/myapp-{random.randrange(10 ** 8)}"
)
def package(self, source: dagger.Directory) -> dagger.Container:
"""Create a production image"""
return (
dag.container()
.from_("nginx:1.25-alpine")
.with_directory("/usr/share/nginx/html", self.build(source))
.with_exposed_port(80)
)
@function
def build(self, source: dagger.Directory) -> dagger.Directory:
"""Create a production build"""
return (
dag.node(ctr=self.build_base_image(source))
.build()
.container()
.directory("./dist")
)
@function
async def test(self, source: dagger.Directory) -> str:
"""Run unit tests"""
return await (
dag.node(ctr=self.build_base_image(source))
.commands()
.run(["test:unit", "run"])
.stdout()
)
def build_base_image(self, source: dagger.Directory) -> dagger.Container:
"""Build base image"""
return (
dag.node(version="21").with_npm().with_source(source).install().container()
)
This function simply calls the package()
function created earlier to obtain the container image and then returns it as a service using the Container.as_service()
function.
import {
dag,
Container,
Directory,
object,
func,
Service,
} from "@dagger.io/dagger"
@object()
class MyModule {
/*
* Create a service from the production image
*/
@func()
serve(source: Directory): Service {
return this.package(source).asService()
}
/*
* Publish an image
*/
@func()
async publish(source: Directory): Promise<string> {
return await this.package(source).publish(
"ttl.sh/myapp-" + Math.floor(Math.random() * 10000000),
)
}
/*
* Create a production image
*/
@func()
package(source: Directory): Container {
return dag
.container()
.from("nginx:1.25-alpine")
.withDirectory("/usr/share/nginx/html", this.build(source))
.withExposedPort(80)
}
/*
* Create a production build
*/
@func()
build(source: Directory): Directory {
return dag
.node({ ctr: this.buildBaseImage(source) })
.commands()
.build()
.directory("./dist")
}
/*
* Run unit tests
*/
@func()
async test(source: Directory): Promise<string> {
return await dag
.node({ ctr: this.buildBaseImage(source) })
.commands()
.run(["test:unit", "run"])
.stdout()
}
/*
* Build base image
*/
buildBaseImage(source: Directory): Container {
return dag
.node({ version: "21" })
.withNpm()
.withSource(source)
.install([])
.container()
}
}
This function simply calls the package()
function created earlier to obtain the container image and then returns it as a service using the Container.asService()
function.
Try the function by running the command below:
dagger call serve --source=. up --ports=8080:80
You should now be able to access the application by browsing to http://localhost:8080 on the host (replace localhost
with your Docker host's network name if accessing it remotely).
The --ports 8080:80
argument results in container port 80 being mapped to host port 8080. An alternative is to not provide any --ports
flags, which results in the exposed ports on the container being auto-mapped to the corresponding ports on the host (i.e. port 80
will be used on your localhost).
Conclusion
This guide walked you through the process of creating a Dagger Module to encapsulate common CI pipeline operations for an application. It explained how to create a module, add functions to it, and work with function inputs and outputs. It also demonstrated how to use modules developed by the Dagger community to speed up your development.
Appendix A: Create an example application
This tutorial assumes that you have a Node.js Web application. If not, create a simple TypeScript application using the Vue framework. Run the command below, answer "Yes" to all the prompts and select "Cypress" as the testing tool:
npm create vue@latest myapp