Build, Test and Publish a Spring Application with Dagger
Introduction
Dagger SDKs are currently available for Go, Node.js and Python, but you can use them to create CI/CD pipelines for applications written in any programming language. This guide explains how to use Dagger to continuously build, test and publish a Java application using Spring. You will learn how to:
- Create a Dagger pipeline to:
- Build your Spring application with all required dependencies
- Run unit tests for your Spring application
- Publish the final application image to Docker Hub
- Run the Dagger pipeline on the local host using the Dagger CLI
- Run the Dagger pipeline on every repository commit using GitHub Actions
Requirements
This guide assumes that:
- You have a basic understanding of how Dagger works. If not, read the Dagger Quickstart.
- You have a basic understanding of GitHub Actions. If not, learn about GitHub Actions.
- You have Docker installed and running in your development environment. If not, install Docker.
- You have the Dagger CLI installed in your development environment. If not, install the Dagger CLI.
- You have a Docker Hub account. If not, register for a free Docker Hub account.
- You have a GitHub account. If not, register for a free GitHub account.
- You have a GitHub repository containing a Spring application. This repository should also be cloned locally in your development environment. If not, follow the steps in Appendix A to create and populate a local and GitHub repository with a Spring sample application.
Step 1: Create the Dagger pipeline
The first step is to create a Dagger pipeline to build and test a container image of the application, and publish it to Docker Hub
- Go
- Node.js
- Python
-
In the application directory, install the Dagger SDK:
go mod init main
go get dagger.io/dagger@latest -
Create a new sub-directory named
ci
. Within theci
directory, create a file namedmain.go
and add the following code to it.package main
import (
"context"
"fmt"
"log"
"os"
"dagger.io/dagger"
)
func main() {
// check for Docker Hub registry credentials in host environment
vars := []string{"DOCKERHUB_USERNAME", "DOCKERHUB_PASSWORD"}
for _, v := range vars {
if os.Getenv(v) == "" {
log.Fatalf("Environment variable %s is not set", v)
}
}
// initialize Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// set registry password as secret for Dagger pipeline
password := client.SetSecret("password", os.Getenv("DOCKERHUB_PASSWORD"))
username := os.Getenv("DOCKERHUB_USERNAME")
// create a cache volume for Maven downloads
mavenCache := client.CacheVolume("maven-cache")
// get reference to source code directory
source := client.Host().Directory(".", dagger.HostDirectoryOpts{
Exclude: []string{"ci"},
})
// create database service container
mariadb := client.Container().
From("mariadb:10.11.2").
WithEnvVariable("MARIADB_USER", "petclinic").
WithEnvVariable("MARIADB_PASSWORD", "petclinic").
WithEnvVariable("MARIADB_DATABASE", "petclinic").
WithEnvVariable("MARIADB_ROOT_PASSWORD", "root").
WithExposedPort(3306).
AsService()
// use maven:3.9 container
// mount cache and source code volumes
// set working directory
app := client.Container().
From("maven:3.9-eclipse-temurin-17").
WithMountedCache("/root/.m2", mavenCache).
WithMountedDirectory("/app", source).
WithWorkdir("/app")
// define binding between
// application and service containers
// define JDBC URL for tests
// test, build and package application as JAR
build := app.WithServiceBinding("db", mariadb).
WithEnvVariable("MYSQL_URL", "jdbc:mysql://petclinic:petclinic@db/petclinic").
WithExec([]string{"mvn", "-Dspring.profiles.active=mysql", "clean", "package"})
// use eclipse alpine container as base
// copy JAR files from builder
// set entrypoint and database profile
deploy := client.Container().
From("eclipse-temurin:17-alpine").
WithDirectory("/app", build.Directory("./target")).
WithEntrypoint([]string{"java", "-jar", "-Dspring.profiles.active=mysql", "/app/spring-petclinic-3.0.0-SNAPSHOT.jar"})
// publish image to registry
address, err := deploy.WithRegistryAuth("docker.io", username, password).
Publish(ctx, fmt.Sprintf("%s/myapp", username))
if err != nil {
panic(err)
}
// print image address
fmt.Println("Image published at:", address)
}This Dagger pipeline performs a number of different operations:
- It imports the Dagger SDK and checks for Docker Hub registry credentials in the host environment. It also creates a Dagger client with
dagger.Connect()
. This client provides an interface for executing commands against the Dagger engine. - It uses the client's
SetSecret()
method to set the Docker Hub registry password as a secret for the Dagger pipeline and configures a Maven cache volume with theCacheVolume()
method. This cache volume is used to persist the state of the Maven cache between runs, thereby eliminating time spent on re-downloading Maven packages. - It uses the client's
Host().Directory()
method to obtain a reference to the source code directory on the host. - It uses the client's
Container().From()
method to initialize three new containers, each of which is returned as aContainer
object:- A MariaDB database service container from the
mariadb:10.11.2
image, for application unit tests; - A Maven container with all required tools and dependencies from the
maven:3.9-eclipse-temurin-17
image, to build and package the application JAR file; - An OpenJDK Eclipse Temurin container from the
eclipse-temurin:17-alpine
image, to create an optimized deployment package.
- A MariaDB database service container from the
- For the MariaDB database container:
- It chains multiple
WithEnvVariable()
methods to configure the database service, and uses theWithExposedPort()
andAsService()
methods to ensure that the service is available to clients.
- It chains multiple
- For the Maven container:
- It uses the
WithMountedDirectory()
andWithMountedCache()
methods to mount the host directory and the cache volume into the Maven container at the/src
and/root/.m2
mount points, and theWithWorkdir()
method to set the working directory in the container. - It adds a service binding for the database service to the Maven container using the
WithServiceBinding()
method and sets the JDBC URL for the application test suite as an environment using theWith_EnvVariable()
method. - Finally, it uses the
WithExec()
method to execute themvn -Dspring.profiles.active=mysql clean package
command, which builds, tests and creates a JAR package of the application.
- It uses the
- For the Eclipse Temurin container:
- Once the JAR package is ready, it copies only the build artifact directory to the Eclipse Temurin container using the
WithDirectory()
method, and sets the container entrypoint to start the Spring application using theWithEntrypoint()
method.
- Once the JAR package is ready, it copies only the build artifact directory to the Eclipse Temurin container using the
- It uses the
WithRegistryAuth()
method to set the registry credentials (including the password set as a secret previously) and then invokes thePublish()
method to publish the Eclipse Temurin container image to Docker Hub. It also prints the SHA identifier of the published image.
- It imports the Dagger SDK and checks for Docker Hub registry credentials in the host environment. It also creates a Dagger client with
-
Run the following command to update
go.sum
:go mod tidy
-
Begin by installing the Dagger SDK as a development dependency:
npm install @dagger.io/dagger@latest --save-dev
-
Create a new sub-directory named
ci
. Within theci
directory, create a file namedindex.mjs
and add the following code to it.import { connect } from "@dagger.io/dagger"
// check for Docker Hub registry credentials in host environment
const vars = ["DOCKERHUB_USERNAME", "DOCKERHUB_PASSWORD"]
vars.forEach((v) => {
if (!process.env[v]) {
console.log(`${v} variable must be set`)
process.exit()
}
})
connect(
async (client) => {
const username = process.env.DOCKERHUB_USERNAME
// set registry password as secret for Dagger pipeline
const password = client.setSecret(
"password",
process.env.DOCKERHUB_PASSWORD,
)
// create a cache volume for Maven downloads
const mavenCache = client.cacheVolume("maven-cache")
// get reference to source code directory
const source = client.host().directory(".", { exclude: ["ci/"] })
// create database service container
const mariadb = client
.container()
.from("mariadb:10.11.2")
.withEnvVariable("MARIADB_USER", "petclinic")
.withEnvVariable("MARIADB_PASSWORD", "petclinic")
.withEnvVariable("MARIADB_DATABASE", "petclinic")
.withEnvVariable("MARIADB_ROOT_PASSWORD", "root")
.withExposedPort(3306)
.asService()
// use maven:3.9 container
// mount cache and source code volumes
// set working directory
const app = client
.container()
.from("maven:3.9-eclipse-temurin-17")
.withMountedCache("/root/.m2", mavenCache)
.withMountedDirectory("/app", source)
.withWorkdir("/app")
// define binding between
// application and service containers
// define JDBC URL for tests
// test, build and package application as JAR
const build = app
.withServiceBinding("db", mariadb)
.withEnvVariable(
"MYSQL_URL",
"jdbc:mysql://petclinic:petclinic@db/petclinic",
)
.withExec(["mvn", "-Dspring.profiles.active=mysql", "clean", "package"])
// use eclipse alpine container as base
// copy JAR files from builder
// set entrypoint and database profile
const deploy = client
.container()
.from("eclipse-temurin:17-alpine")
.withDirectory("/app", build.directory("./target"))
.withEntrypoint([
"java",
"-jar",
"-Dspring.profiles.active=mysql",
"/app/spring-petclinic-3.0.0-SNAPSHOT.jar",
])
// publish image to registry
const address = await deploy
.withRegistryAuth("docker.io", username, password)
.publish(`${username}/myapp`)
// print image address
console.log(`Image published at: ${address}`)
},
{ LogOutput: process.stderr },
)This Dagger pipeline performs a number of different operations:
- It imports the Dagger SDK and checks for Docker Hub registry credentials in the host environment. It also creates a Dagger client with
connect()
. This client provides an interface for executing commands against the Dagger engine. - It uses the client's
setSecret()
method to set the Docker Hub registry password as a secret for the Dagger pipeline and configures a Maven cache volume with thecacheVolume()
method. This cache volume is used to persist the state of the Maven cache between runs, thereby eliminating time spent on re-downloading Maven packages. - It uses the client's
host().directory()
method to obtain a reference to the source code directory on the host. - It uses the client's
container().from()
method to initialize three new containers, each of which is returned as aContainer
object:- A MariaDB database service container from the
mariadb:10.11.2
image, for application unit tests; - A Maven container with all required tools and dependencies from the
maven:3.9-eclipse-temurin-17
image, to build and package the application JAR file; - An OpenJDK Eclipse Temurin container from the
eclipse-temurin:17-alpine
image, to create an optimized deployment package.
- A MariaDB database service container from the
- For the MariaDB database container:
- It chains multiple
withEnvVariable()
methods to configure the database service, and uses thewithExposedPort()
andservice()
methods to ensure that the service is available to clients.
- It chains multiple
- For the Maven container:
- It uses the
withMountedDirectory()
andwithMountedCache()
methods to mount the host directory and the cache volume into the Maven container at the/src
and/root/.m2
mount points, and thewithWorkdir()
method to set the working directory in the container. - It adds a service binding for the database service to the Maven container using the
withServiceBinding()
method and sets the JDBC URL for the application test suite as an environment using thewithEnvVariable()
method. - Finally, it uses the
withExec()
method to execute themvn -Dspring.profiles.active=mysql clean package
command, which builds, tests and creates a JAR package of the application.
- It uses the
- For the Eclipse Temurin container:
- Once the JAR package is ready, it copies only the build artifact directory to the Eclipse Temurin container using the
withDirectory()
method, and sets the container entrypoint to start the Spring application using thewithEntrypoint()
method.
- Once the JAR package is ready, it copies only the build artifact directory to the Eclipse Temurin container using the
- It uses the
withRegistryAuth()
method to set the registry credentials (including the password set as a secret previously) and then invokes thepublish()
method to publish the Eclipse Temurin container image to Docker Hub. It also prints the SHA identifier of the published image.
- It imports the Dagger SDK and checks for Docker Hub registry credentials in the host environment. It also creates a Dagger client with
-
Begin by creating a virtual environment and installing the Dagger SDK:
pip install dagger-io
-
Create a new sub-directory named
ci
. Within theci
directory, create a file namedmain.py
and add the following code to it.import os
import sys
import anyio
import dagger
async def main():
# check for Docker Hub registry credentials in host environment
for var in ["DOCKERHUB_USERNAME", "DOCKERHUB_PASSWORD"]:
if var not in os.environ:
msg = f"{var} environment variable must be set"
raise OSError(msg)
# initialize Dagger client
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
username = os.environ["DOCKERHUB_USERNAME"]
# set registry password as secret for Dagger pipeline
password = client.set_secret("password", os.environ["DOCKERHUB_PASSWORD"])
# create a cache volume for Maven downloads
maven_cache = client.cache_volume("maven-cache")
# get reference to source code directory
source = client.host().directory(".", exclude=["ci", ".venv"])
# create database service container
mariadb = (
client.container()
.from_("mariadb:10.11.2")
.with_env_variable("MARIADB_USER", "petclinic")
.with_env_variable("MARIADB_PASSWORD", "petclinic")
.with_env_variable("MARIADB_DATABASE", "petclinic")
.with_env_variable("MARIADB_ROOT_PASSWORD", "root")
.with_exposed_port(3306)
.as_service()
)
# use maven:3.9 container
# mount cache and source code volumes
# set working directory
app = (
client.container()
.from_("maven:3.9-eclipse-temurin-17")
.with_mounted_cache("/root/.m2", maven_cache)
.with_mounted_directory("/app", source)
.with_workdir("/app")
)
# define binding between
# application and service containers
# define JDBC URL for tests
# test, build and package application as JAR
build = (
app.with_service_binding("db", mariadb)
.with_env_variable(
"MYSQL_URL",
"jdbc:mysql://petclinic:petclinic@db/petclinic",
)
.with_exec(["mvn", "-Dspring.profiles.active=mysql", "clean", "package"])
)
# use eclipse alpine container as base
# copy JAR files from builder
# set entrypoint and database profile
deploy = (
client.container()
.from_("eclipse-temurin:17-alpine")
.with_directory("/app", build.directory("./target"))
.with_entrypoint(
[
"java",
"-jar",
"-Dspring.profiles.active=mysql",
"/app/spring-petclinic-3.0.0-SNAPSHOT.jar",
]
)
)
# publish image to registry
address = await deploy.with_registry_auth(
"docker.io", username, password
).publish(f"{username}/myapp")
# print image address
print(f"Image published at: {address}")
anyio.run(main)This Dagger pipeline performs a number of different operations:
- It imports the Dagger SDK and checks for Docker Hub registry credentials in the host environment. It also creates a Dagger client with
dagger.Connection()
. This client provides an interface for executing commands against the Dagger engine. - It uses the client's
set_secret()
method to set the Docker Hub registry password as a secret for the Dagger pipeline and configures a Maven cache volume with thecache_volume()
method. This cache volume is used to persist the state of the Maven cache between runs, thereby eliminating time spent on re-downloading Maven packages. - It uses the client's
host().directory()
method to obtain a reference to the source code directory on the host. - It uses the client's
container().from_()
method to initialize three new containers, each of which is returned as aContainer
object:- A MariaDB database service container from the
mariadb:10.11.2
image, for application unit tests; - A Maven container with all required tools and dependencies from the
maven:3.9-eclipse-temurin-17
image, to build and package the application JAR file; - An OpenJDK Eclipse Temurin container from the
eclipse-temurin:17-alpine
image, to create an optimized deployment package.
- A MariaDB database service container from the
- For the MariaDB database container:
- It chains multiple
with_env_variable()
methods to configure the database service, and uses thewith_exposed_port()
andservice()
methods to ensure that the service is available to clients.
- It chains multiple
- For the Maven container:
- It uses the
with_mounted_directory()
andwith_mounted_cache()
methods to mount the host directory and the cache volume into the Maven container at the/src
and/root/.m2
mount points, and thewith_workdir()
method to set the working directory in the container. - It adds a service binding for the database service to the Maven container using the
with_service_binding()
method and sets the JDBC URL for the application test suite as an environment using thewith_env_variable()
method. - Finally, it uses the
with_exec()
method to execute themvn -Dspring.profiles.active=mysql clean package
command, which builds, tests and creates a JAR package of the application.
- It uses the
- For the Eclipse Temurin container:
- Once the JAR package is ready, it copies only the build artifact directory to the Eclipse Temurin container using the
with_directory()
method, and sets the container entrypoint to start the Spring application using thewith_entrypoint()
method.
- Once the JAR package is ready, it copies only the build artifact directory to the Eclipse Temurin container using the
- It uses the
with_registry_auth()
method to set the registry credentials (including the password set as a secret previously) and then invokes thepublish()
method to publish the Eclipse Temurin container image to Docker Hub. It also prints the SHA identifier of the published image.
- It imports the Dagger SDK and checks for Docker Hub registry credentials in the host environment. It also creates a Dagger client with
Step 2: Test the Dagger pipeline on the local host
Configure the registry credentials using environment variables on the local host. Replace the USERNAME
and PASSWORD
placeholders with your Docker Hub credentials.
export DOCKERHUB_USERNAME=USERNAME
export DOCKERHUB_PASSWORD=PASSWORD
Once credentials are configured, test the Dagger pipeline by running the command below:
- Go
- Node.js
- Python
dagger run go run ci/main.go
dagger run node ci/index.mjs
dagger run python ci/main.py
The dagger run
command executes the script in a Dagger session and displays live progress. At the end of the process, the built container is published on Docker Hub and a message similar to the one below appears in the console output:
Image published at: docker.io/.../myapp@sha256:...
Step 3: Create a GitHub Actions workflow
Dagger executes your pipelines entirely as standard OCI containers. This means that the same pipeline will run the same, whether on on your local machine or a remote server.
This also means that it's very easy to move your Dagger pipeline from your local host to GitHub Actions - all that's needed is to commit and push the pipeline script from your local clone to your GitHub repository, and then define a GitHub Actions workflow to run it on every commit.
-
Commit and push the pipeline script and related changes to the application's GitHub repository:
git add .
git commit -a -m "Added pipeline"
git push -
In the GitHub repository, create a new workflow file at
.github/workflows/main.yml
with the following content:- Go
- Node.js
- Python
name: 'ci'
on:
push:
branches:
- main
jobs:
dagger:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
-
name: Setup go
uses: actions/setup-go@v4
with:
go-version: '>=1.20'
-
name: Install Dagger
run: go get dagger.io/dagger@latest
-
name: Install Dagger CLI
run: cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sh; cd -; }
-
name: Build and publish with Dagger
run: dagger run go run ci/main.go
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}name: 'ci'
on:
push:
branches:
- main
jobs:
dagger:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
-
name: Setup node
uses: actions/setup-node@v3
with:
node-version: 18
cache: npm
-
name: Install Dagger
run: npm install @dagger.io/dagger@latest
-
name: Install Dagger CLI
run: cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sh; cd -; }
-
name: Build and publish with Dagger
run: dagger run node ci/index.mjs
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}name: 'ci'
on:
push:
branches:
- main
jobs:
dagger:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
-
name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
-
name: Install Dagger
run: pip install dagger-io
-
name: Install Dagger CLI
run: cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sh; cd -; }
-
name: Build and publish with Dagger
run: dagger run python ci/main.py
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}This workflow runs on every commit to the repository
main
branch. It consists of a single job with six steps, as below:- The first step uses the Checkout action to check out the latest source code from the
main
branch to the GitHub runner. - The second step uses the Docker Login action to authenticate to Docker Hub from the GitHub runner. This is necessary because Docker rate-limits unauthenticated registry pulls.
- The third step downloads and installs the required programming language on the GitHub runner.
- The fourth and fifth steps download and install the Dagger SDK and the Dagger CLI on the GitHub runner.
- The final step executes the Dagger pipeline.
The Docker Login action and the Dagger pipeline both expect to find Docker Hub credentials in the DOCKERHUB_USERNAME
and DOCKERHUB_PASSWORD
variables. Create these variables as GitHub secrets as follows:
- Navigate to the
Settings
->Secrets and variables
->Actions
page of the GitHub repository. - Click
New repository secret
to create a new secret. - Configure the secret with the following inputs:
- Name:
DOCKERHUB_USERNAME
- Secret: Your Docker Hub username
- Name:
- Click
Add secret
to save the secret. - Repeat the process for the
DOCKERHUB_PASSWORD
variable.
Step 4: Test the Dagger pipeline on GitHub
Test the Dagger pipeline by committing a change to the GitHub repository.
If you are using the Spring Petclinic example application described in Appendix A, the following commands modify and commit a simple change to the application's welcome page:
git pull
sed -i -e "s/Welcome/Welcome from Dagger/g" src/main/resources/messages/messages.properties
git add src/main/resources/messages/messages.properties
git commit -a -m "Update welcome message"
git push
The commit triggers the GitHub Actions workflow defined in Step 3. The workflow runs the various steps of the job, including the pipeline script.
At the end of the process, a new version of the built container image is published to Docker Hub. A message similar to the one below appears in the GitHub Actions log:
Image published at: docker.io/.../myapp@sha256:...
Test the container, replacing IMAGE-ADDRESS
with the image address returned by the pipeline.
docker run --rm --detach --net=host --name mariadb -e MYSQL_USER=user -e MYSQL_PASSWORD=password -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=db mariadb:10.11.2
docker run --rm --net=host -e MYSQL_URL=jdbc:mysql://user:password@localhost/db IMAGE-ADDRESS
Browse to host port 8080. If you are using the Spring Petclinic example application described in Appendix A, you see the page shown below:
Conclusion
Dagger SDKs are currently available for Go, Node.js and Python, but you can use Dagger to create CI/CD pipelines for applications written in any programming language. This tutorial demonstrated by creating a Dagger pipeline to build, test and publish a Spring application. A similar approach can be followed for any application, regardless of which programming language it's written in.
Use the API Key Concepts page and the Go, Node.js and Python SDK References to learn more about Dagger.
Appendix A: Create a GitHub repository with an example Spring application
This tutorial assumes that you have a GitHub repository with a Spring application. If not, follow the steps below to create a GitHub repository and commit an example Express application to it.
This section assumes that you have the GitHub CLI. If not, install the GitHub CLI before proceeding.
- Log in to GitHub using the GitHub CLI:
gh auth login
- Create a directory for the Spring application:
mkdir myapp
cd myapp
- Clone the Spring Petclinic sample application:
git clone git@github.com:spring-projects/spring-petclinic.git .
- Update the
.gitignore
file:
echo node_modules >> .gitignore
echo package*.json >> .gitignore
echo .venv >> .gitignore
git add .
git commit -m "Updated .gitignore"
- Remove existing GitHub Action workflows:
rm -rf .github/workflows/*
git add .
git commit -m "Removed workflows"
- Create a private repository in your GitHub account and push the code to it:
gh repo create myapp --push --source . --private --remote github