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
- PHP
- Java
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,
// +defaultPath="/"
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(
// +defaultPath="/"
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,
// +defaultPath="/"
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(
// +defaultPath="/"
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
from typing import Annotated
import dagger
from dagger import DefaultPath, dag, function, object_type
@object_type
class HelloDagger:
@function
async def publish(
self,
source: Annotated[dagger.Directory, DefaultPath("/")],
) -> 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: Annotated[dagger.Directory, DefaultPath("/")],
) -> 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: Annotated[dagger.Directory, DefaultPath("/")],
) -> 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: Annotated[dagger.Directory, DefaultPath("/")],
) -> 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,
argument,
} from "@dagger.io/dagger"
@object()
class HelloDagger {
/**
* Publish the application container after building and testing it on-the-fly
*/
@func()
async publish(
@argument({ defaultPath: "/" }) 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(@argument({ defaultPath: "/" }) 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(
@argument({ defaultPath: "/" }) 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(@argument({ defaultPath: "/" }) source: Directory): Container {
return dag
.node({ version: "21" })
.withNpm()
.withSource(source)
.install()
.container()
}
}
Update the dagger/src/HelloDagger.php
file with the following code:
<?php
declare(strict_types=1);
namespace DaggerModule;
use Dagger\Attribute\DaggerFunction;
use Dagger\Attribute\DaggerObject;
use Dagger\Attribute\DefaultPath;
use Dagger\Attribute\Doc;
use Dagger\Container;
use Dagger\Directory;
use function Dagger\dag;
#[DaggerObject]
class HelloDagger
{
#[DaggerFunction]
#[Doc('Publish the application container after building and testing it on-the-fly')]
public function publish(
#[DefaultPath('/')]
Directory $source,
): string
{
$this->test($source);
return $this
->build($source)
->publish('ttl.sh/hello-dagger-' . rand(0, 10000000));
}
#[DaggerFunction]
#[Doc('Build the application container')]
public function build(
#[DefaultPath('/')]
Directory $source
): Container
{
$build = dag()
->node(null, $this->buildEnv($source))
->commands()
->run(['build'])
->directory('./dist');
return dag()
->container()
->from('nginx:1.25-alpine')
->withDirectory('/usr/share/nginx/html', $build)
->withExposedPort(80);
}
#[DaggerFunction]
#[Doc('Return the result of running unit tests')]
public function test(
#[DefaultPath('/')]
Directory $source,
): string
{
return dag()
->node(null, $this->buildEnv($source))
->commands()
->run(['test:unit', 'run'])
->stdout();
}
#[DaggerFunction]
#[Doc('Build a ready-to-use development environment')]
public function buildEnv(
#[DefaultPath('/')]
Directory $source,
): Container
{
return dag()
->node('21')
->withNpm()
->withSource($source)
->install()
->container();
}
}
Update the dagger/src/main/java/io/dagger/modules/hellodagger/HelloDagger.java
file with the following code:
package io.dagger.modules.hellodagger;
import io.dagger.client.*;
import io.dagger.module.AbstractModule;
import io.dagger.module.annotation.DefaultPath;
import io.dagger.module.annotation.Function;
import io.dagger.module.annotation.Object;
import java.util.List;
import java.util.concurrent.ExecutionException;
/** HelloDagger main object */
@Object
public class HelloDagger extends AbstractModule {
/** Publish the application container after building and testing it on-the-fly */
@Function
public String publish(@DefaultPath("/") Directory source)
throws ExecutionException, DaggerQueryException, InterruptedException {
this.test(source);
return build(source)
.publish("ttl.sh/hello-dagger-%d".formatted((int) (Math.random() * 10000000)));
}
/** Build the application container */
@Function
public Container build(@DefaultPath("/") Directory source) {
Directory build =
dag.node(new Client.NodeArguments().withCtr(buildEnv(source)))
.commands()
.run(List.of("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 */
@Function
public String test(@DefaultPath("/") Directory source)
throws ExecutionException, DaggerQueryException, InterruptedException {
return dag.node(new Client.NodeArguments().withCtr(buildEnv(source)))
.commands()
.run(List.of("test:unit", "run"))
.stdout();
}
/** Build a ready-to-use development environment */
@Function
public Container buildEnv(@DefaultPath("/") Directory source) {
CacheVolume nodeCache = dag.cacheVolume("node");
return dag.node(new Client.NodeArguments().withVersion("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, TypeScript, PHP, or Java. 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.
There's one other improvement in this revised pipeline. Every Dagger Function now specifies a default path (/
) for the source
directory argument. This means that if no source
argument is provided on the command line, Dagger will default to the root of the Git repository as the location of the application's source code.
Default paths are only available for Directory
and File
arguments. Dagger's default path resolution differs depending on whether the paths are (a) absolute or relative; and (b) in Git or non-Git contexts.
Run the pipeline
Run the pipeline:
dagger call publish
As before, you should see the application being tested, built, and published to the ttl.sh container registry. With the default path in place, everything works as before, even though the source
argument is no longer specified in the Dagger Function call.
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.