Skip to main content

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:

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

  1. In the application directory, install the Dagger SDK:

    go mod init main
    go get dagger.io/dagger@latest
  2. Create a new sub-directory named ci. Within the ci directory, create a file named main.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).
    WithExec([]string{})

    // 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.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 the cache_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 a Container 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.
    • For the MariaDB database container:
      • It chains multiple with_env_variable() methods to configure the database service, and uses the with_exposed_port() method to ensure that the service is available to clients.
    • For the Maven container:
      • It uses the with_mounted_directory() and with_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 the with_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 the with_env_variable() method.
      • Finally, it uses the with_exec() method to execute the mvn -Dspring.profiles.active=mysql clean package command, which builds, tests and creates a JAR package of the application.
    • 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 the with_entrypoint() method.
    • It uses the with_registry_auth() method to set the registry credentials (including the password set as a secret previously) and then invokes the publish() method to publish the Eclipse Temurin container image to Docker Hub. It also prints the SHA identifier of the published image.
  3. Run the following command to update go.sum:

    go mod tidy

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:

dagger run go run ci/main.go

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.

  1. 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
  2. In the GitHub repository, create a new workflow file at .github/workflows/main.yml with the following content:

    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 }}

    This workflow runs on every commit to the repository main branch. It consists of a single job with six steps, as below:

    1. The first step uses the Checkout action to check out the latest source code from the main branch to the GitHub runner.
    2. 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.
    3. The third step downloads and installs the required programming language on the GitHub runner.
    4. The fourth and fifth steps download and install the Dagger SDK and the Dagger CLI on the GitHub runner.
    5. 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:

  1. Navigate to the Settings -> Secrets and variables -> Actions page of the GitHub repository.
  2. Click New repository secret to create a new secret.
  3. Configure the secret with the following inputs:
    • Name: DOCKERHUB_USERNAME
    • Secret: Your Docker Hub username
  4. Click Add secret to save the secret.
  5. Repeat the process for the DOCKERHUB_PASSWORD variable.

Create GitHub secret

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:

Application welcome page

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.

info

This section assumes that you have the GitHub CLI. If not, install the GitHub CLI before proceeding.

  1. Log in to GitHub using the GitHub CLI:

    gh auth login
  2. Create a directory for the Spring application:

    mkdir myapp
    cd myapp
  3. Clone the Spring Petclinic sample application:

    git clone git@github.com:spring-projects/spring-petclinic.git .
  4. Update the .gitignore file:

    echo node_modules >> .gitignore
    echo package*.json >> .gitignore
    echo .venv >> .gitignore
    git add .
    git commit -m "Updated .gitignore"
  5. Remove existing GitHub Action workflows:

    rm -rf .github/workflows/*
    git add .
    git commit -m "Removed workflows"
  6. Create a private repository in your GitHub account and push the code to it:

    gh repo create myapp --push --source . --private --remote github