Quickstart
Simplify with the Daggerverse
At this point, you have successfully built a pipeline for an application using the Dagger API. This is important knowledge to have, so that you understand the basics of using Dagger and can begin creating pipelines for your own applications.
That said, Dagger also lets you use Dagger Functions developed by others and published to the Daggerverse, Dagger's free and publicly-available index of Dagger modules.
Dagger's superpower is its community. All around the world, Daggernauts are encoding their expertise into Dagger Functions, and sharing them for anyone to reuse. The Daggerverse is a free service run by Dagger, which indexes all publicly available Dagger modules, and lets you easily search and consume them. Using the Daggerverse, you can easily discover great modules being developed by the community, learn how they work, and start using them. Since modules are just source code, it's easy to contribute to them, too!
Using the Daggerverse is optional, and does not change how you use Dagger. If you find a module you like, simply copy its URL, and use it the usual way.
To understand how this works in practice, let's simplify the pipeline using the Node module from the Daggerverse. This module contains tested, ready-to-use Dagger Functions for building, linting and testing Node.js applications.
Install a Daggerverse module
Install the Node module as a dependency by running the dagger install
command shown on its Daggerverse page:
dagger install github.com/dagger/dagger/sdk/typescript/dev/node@f1aa5e117e163449208519ce6c6b1dbdb8ef5d79
The exact Git commit for the module version is recorded in dagger.json
. Dagger enforces version pinning, which guarantees that the module version you install is the one you'll be using.
Once you've installed the Node module, start exploring it to see what you can do with it. There are various ways to do this:
- Read the module's auto-generated documentation in the Daggerverse.
- Inspect the module's source code.
- Use the Dagger CLI to inspect the module with the command
dagger -m node functions
ordagger -m node call --help
.
Simplify the pipeline
Next, update the pipeline to use this module.
- Go
- Python
- TypeScript
Update the dagger/main.go
file with the following code:
package main
import (
"context"
"fmt"
"math"
"math/rand"
"dagger/hello-dagger/internal/dagger"
)
type HelloDagger struct{}
// Publish the application container after building and testing it on-the-fly
func (m *HelloDagger) Publish(ctx context.Context, source *dagger.Directory) (string, error) {
_, err := m.Test(ctx, source)
if err != nil {
return "", err
}
address, err := m.Build(source).
Publish(ctx, fmt.Sprintf("ttl.sh/hello-dagger-%.0f", math.Floor(rand.Float64()*10000000))) //#nosec
if err != nil {
return "", err
}
return address, nil
}
// Build the application container
func (m *HelloDagger) Build(source *dagger.Directory) *dagger.Container {
build := dag.Node(dagger.NodeOpts{Ctr: m.BuildEnv(source)}).
Commands().
Run([]string{"build"}).
Directory("./dist")
return dag.Container().From("nginx:1.25-alpine").
WithDirectory("/usr/share/nginx/html", build).
WithExposedPort(80)
}
// Return the result of running unit tests
func (m *HelloDagger) Test(ctx context.Context, source *dagger.Directory) (string, error) {
return dag.Node(dagger.NodeOpts{Ctr: m.BuildEnv(source)}).
Commands().
Run([]string{"test:unit", "run"}).
Stdout(ctx)
}
// Build a ready-to-use development environment
func (m *HelloDagger) BuildEnv(source *dagger.Directory) *dagger.Container {
return dag.Node(dagger.NodeOpts{Version: "21"}).
WithNpm().
WithSource(source).
Install().
Container()
}
Update the dagger/src/hello_dagger/main.py
file with the following code:
import random
import dagger
from dagger import dag, function, object_type
@object_type
class HelloDagger:
@function
async def publish(self, source: dagger.Directory) -> str:
"""Publish the application container after building and testing it on-the-fly"""
await self.test(source)
return await self.build(source).publish(
f"ttl.sh/hello-dagger-{random.randrange(10 ** 8)}"
)
@function
def build(self, source: dagger.Directory) -> dagger.Container:
"""Build the application container"""
build = (
dag.node(ctr=self.build_env(source))
.commands()
.run(["build"])
.directory("./dist")
)
return (
dag.container()
.from_("nginx:1.25-alpine")
.with_directory("/usr/share/nginx/html", build)
.with_exposed_port(80)
)
@function
async def test(self, source: dagger.Directory) -> str:
"""Return the result of running unit tests"""
return await (
dag.node(ctr=self.build_env(source))
.commands()
.run(["test:unit", "run"])
.stdout()
)
@function
def build_env(self, source: dagger.Directory) -> dagger.Container:
"""Build a ready-to-use development environment"""
return (
dag.node(version="21").with_npm().with_source(source).install().container()
)
Update the dagger/src/index.ts
file with the following code:
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
class HelloDagger {
/**
* Publish the application container after building and testing it on-the-fly
*/
@func()
async publish(source: Directory): Promise<string> {
await this.test(source)
return await this.build(source).publish(
"ttl.sh/hello-dagger-" + Math.floor(Math.random() * 10000000),
)
}
/**
* Build the application container
*/
@func()
build(source: Directory): Container {
const build = dag
.node({ ctr: this.buildEnv(source) })
.commands()
.run(["build"])
.directory("./dist")
return dag
.container()
.from("nginx:1.25-alpine")
.withDirectory("/usr/share/nginx/html", build)
.withExposedPort(80)
}
/**
* Return the result of running unit tests
*/
@func()
async test(source: Directory): Promise<string> {
return await dag
.node({ ctr: this.buildEnv(source) })
.commands()
.run(["test:unit", "run"])
.stdout()
}
/**
* Build a ready-to-use development environment
*/
@func()
buildEnv(source: Directory): Container {
return dag
.node({ version: "21" })
.withNpm()
.withSource(source)
.install()
.container()
}
}
This code listing revises the Dagger Functions from earlier, replacing calls to the core Dagger API with calls to Dagger Functions from the Node module. This allows you to access pre-defined functionality for working with Node.js applications - for example, obtaining a base image with npm
and cache volumes already configured, or executing common commands to lint, format, test, and build a Node.js codebase.
If you backtrack a little further and inspect the Node module's source code, you'll notice two important things:
- The Node module is written in TypeScript but is called from your Dagger module, which could be in Go, Python or TypeScript. Dagger takes care of the translation via its language-agnostic GraphQL layer.
- The Node module is a Dagger module very similar to the one you built in this quickstart. If you inspect its source code, you'll recognize the
dag
client and many of the core Dagger API methods you used when building your own Dagger Functions.
Run the pipeline
Run the pipeline:
dagger call publish --source=.
As before, you should see the application being tested, built, and published to the ttl.sh container registry.
This quickstart uses the public ttl.sh container registry, but Dagger also supports publishing to private registries, including Docker Hub, GitHub Container Registry, and many others.
Using a Daggerverse module instead of writing your own Dagger Functions is often advantageous because:
- It provides ready-to-use functionality encapsulating the community's knowledge. This allows you to get started quickly and confidently, without needing to "roll your own code" from scratch.
- It is written in accordance with language standards and best practices. Plus, its source code is open, allowing anyone to inspect it and suggest improvements to it.
- It can include useful optimizations. For example, the Node module used here automatically creates and uses cache volumes for application dependencies.
Dagger Functions can call other functions, across languages. So, even though the Node module in this section is written in TypeScript, you can transparently call its functions from another Dagger module written in Go, Python or any other supported language. This means that you no longer need to care which language your CI tooling is written in; you can use the one that you're most comfortable with or that best suits your requirements.
Dagger is able to do this because it uses GraphQL as its low-level language-agnostic API query language. Each Dagger SDK generates native code-bindings for all dependencies, which abstract away the underlying GraphQL queries. This gives you all the benefits of type-checking, code completion and other IDE features when developing Dagger Functions.