Tutorial: Setting up a CI/CD pipeline

CI/CD for Spring Boot Microservices: Part 1

Tomas Fernandez
© Shutterstock / Kodda

How can you get started with Continuous Integration with Spring Boot? In this tutorial, learn how Continuous Integration and Delivery will help you test and prepare a Java app for Docker. This is part one of a tutorial series by Tomas Fernandez. By the end of this article, you will have set up your first CI/CD pipeline and know all about it!

In this tutorial, we will learn how Continuous Integration and Delivery can help us to test and prepare a Java application for Docker.

Continuous Integration (CI) is a software engineering practice in which we test our code on every update. The practice creates a strong feedback loop that reveals errors as soon as they are introduced. Consequently, we can spend more of our time coding features rather than hunting bugs.

Once we’re confident about the code, we can move on to Continuous Delivery (CD). Continuous Delivery goes through all the steps that make the release so each successful update will have a corresponding package—ready for deployment.

We will use Docker for packaging since it’s supported universally across all cloud providers. Furthermore, Docker is a requirement for more advanced deployments such as Kubernetes. In the second part of the tutorial, we’ll learn your we can use Continuous Deployment (also CD) to deploy new versions to Kubernetes at the push of a button.

I believe that practice is the best school. That is why we’ll work with a demo project. As you follow the steps, you’ll learn how CI/CD can help you code faster and better.

Getting ready

Here’s a list of the things you’ll need to get started.

  • Your favorite Java development editor and SDK.
  • A Docker Hub account and Docker.
  • A GitHub account and Git.
  • Curl to test the application.

Let’s get everything ready. First, fork the repository with the demo and clone it to your machine. This is the project’s structure:

The application is built in Java Spring Boot and it exposes some API endpoints. It uses an in-memory H2 database; that will do for testing and development, but once it goes to production we’ll need to switch to a different storage method. The project includes tests, benchmarks and everything needed to create the Docker image.

These are the commands that we can use to test and run the application:

$ mvn clean test
$ mvn clean test -Pintegration-testing
$ mvn spring-boot:run

The project ships with Apache Jmeter to generate benchmarks:

$ mvn clean jmeter:jmeter
$ mvn jmeter:gui

A Dockerfile to create a Docker image is also included:

$ docker build -t semaphore-demo-java-spring

CI/CD Workflow

In this section we’ll examine what’s inside the .semaphore directory. Here we’ll find the entire configuration for the CI/CD workflow.

We’ll use Semaphore as our Continuous Integration solution. Our CI/CD workflow will:

  1. Download Java dependencies.
  2. Build the application JAR.
  3. Run the tests and benchmark. And, if all goes well…
  4. Create a Docker image and push it to Docker Hub.

But first, open your browser at Semaphore and sign up with your GitHub account; that will link up both accounts. Then, download the sem CLI tool and login your machine:

$ sem connect ACCESS_TOKEN

Your authentication can be found by clicking the terminal icon on the top right side of your Semaphore account.
Finally, add the project to Semaphore with sem init:

$ cd semaphore-demo-java-spring
$ sem init

Continuous Integration

SEE ALSO: 8 key Kotlin features that give it an edge over Java

Semaphore will always look for the initial pipeline file at .semaphore/semaphore.yml. A pipeline bundles all the configuration, environment and commands that Semaphore needs do its job. Let’s examine the CI pipeline.

Name and Agent

The pipeline starts with a name and an agent. The agent is the virtual machine type that powers the jobs. Semaphore offers several machine types, we’ll use the free-tier plan e1-standard-2 type with an Ubuntu 18.04.

version: v1.0
name: Java Spring example CI pipeline on Semaphore
    type: e1-standard-2
    os_image: ubuntu1804

Jobs and Blocks

Jobs define the commands that give life to the CI process. Jobs are grouped in blocks. Once all jobs in a block are done, the next block begins.

The first block downloads the dependencies and builds the application JAR without running any tests. The block uses checkout to clone the repository and cache to store and retrieve the Java dependencies:

  - name: "Build"
        - name: MAVEN_OPTS
          value: "-Dmaven.repo.local=.m2"

      - name: Build
          - checkout
          - cache restore
          - mvn -q package jmeter:configure -Dmaven.test.skip=true
          - cache store

The second block has two jobs, one for the unit tests and one for the integration tests. Commands in the prologue run before each job, so it’s a good place to place common setup commands:

  - name: "Test"
        - name: MAVEN_OPTS
          value: "-Dmaven.repo.local=.m2"

          - checkout
          - cache restore
          - cache restore
          - mvn -q test-compile -Dmaven.test.skip=true
      - name: Unit tests
          - mvn test
      - name: Integration tests
          - mvn test -Pintegration-testing

The third block runs the benchmarks:

 - name: "Performance tests"
        - name: MAVEN_OPTS
          value: "-Dmaven.repo.local=.m2"
          - checkout
          - cache restore
      - name: Benchmark
          - java -version
          - java -jar target/spring-pipeline-demo.jar > /dev/null &
          - sleep 20
          - mvn -q jmeter:jmeter
          - mvn jmeter:results


At this point, the CI is complete. If the code passed all the tests we can jump to the next stage: Continuous Delivery. We link up pipelines using promotions. The end of the CI pipeline includes a promotion to start the docker build if there were no errors:

  - name: Dockerize
    pipeline_file: docker-build.yml
      - result: passed

Continuous Delivery

In this section, we’ll review how Semaphore builds the Docker image. Before you can run this pipeline though, you must tell Semaphore how to connect with Docker Hub.

To securely store passwords, Semaphore provides the secrets feature. Create a secret with your Docker Hub username and password. Semaphore will need them to push images into your repository:


To view your secret details:

$ sem get secret dockerhub

Now that we have everything in place for the Continuous Delivery pipeline, let’s examine how it works. Open the

.semaphore/docker-build.yml file

. The pipeline is made of one block with a single job:

  - name: "Build"
        - name: MAVEN_OPTS
          value: "-Dmaven.repo.local=.m2"
        - name: ENVIRONMENT
          value: "dev"

      - name: dockerhub

          - checkout
          - cache restore

      - name: Build and deploy docker container
          - mvn -q package -Dmaven.test.skip=true
          - echo "$DOCKER_PASSWORD" | docker login  --username "$DOCKER_USERNAME" --password-stdin
          - docker pull "$DOCKER_USERNAME"/semaphore-demo-java-spring:latest || true
          - docker build --cache-from "$DOCKER_USERNAME"/semaphore-demo-java-spring:latest --build-arg ENVIRONMENT="${ENVIRONMENT}" -t "$DOCKER_USERNAME"/semaphore-demo-java-spring:latest .
          - docker push "$DOCKER_USERNAME"/semaphore-demo-java-spring:latest

Here,  the prologue pulls the dependencies the cache. Then the build job packages the JAR into a Docker image and pushes it to Docker Hub. The docker build command can run faster if there is an image in the cache, that is why the pipeline attempts to pull the latest image from the repository first.

Testing the Docker image

SEE ALSO: What kind of Java developer are you? Take our Java Quiz to find out!

To start the workflow, make any modification to the code and push it to GitHub:

$ touch some_file
$ git add some_file
$ git commit -m "test workflow"
$ git push origin master

Go to your Semaphore account to see the pipeline working.

My Workflow

After a few seconds the pipeline should be complete:


By now, you should have a ready Docker image in your repository. Let’s give it a go. Pull the newly created image to your machine:

$ docker pull YOUR_DOCKER_USER/semaphore-demo-java-spring:latest

And start it in your machine:

$ docker run -it -p 8080:8080 YOUR_DOCKER_USERNAME/semaphore-demo-java-spring

You can create a user with a POST request:

$ curl -w "\n" -X POST -d '{ "email": "[email protected]", "password": "sekret" }' -H "Content-type: application/json" localhost:8080/users
{"username":"[email protected]"}

With the user created, you can authenticate and see the secure webpage:

$ curl -w "\n" --user [email protected]:sekret localhost:8080/admin/home
<div class="container">
 Welcome <span>[email protected]</span>!

You can also try it with the login page at localhost:8080/admin/home.


You have set up your first CI/CD pipeline. With this system in place, you can work in your code, secure in the feeling that it’s being constantly tested.

In the next tutorial, we’ll see how to do Continuous Deployment to a Kubernetes cluster.


Tomas Fernandez

Tomas studied electronics and computer engineering at Buenos Aires University. Over the last 18 years, he wore many hats: developer, sysadmin, consultant, and dba. Not so long ago, he found his passion for reading turning into a drive for writing. When he’s not working, he enjoys reading, swimming, and sailing.

Inline Feedbacks
View all comments
2 years ago

Nice article. How do you recommend doing this for front-to-back features that also involve database changes?

Reply to  l1cache
2 years ago

Thanks! In the second part of the tutorial, I switch the database backend to MySQL.

For database changes there are basically two options if you are using a RBDMS:

1. Do it manually: harder but safer.
2. Use migrations. The ORM would generate all the necessary ALTER statements. Should test them on a staging copy of the DB first.

2 years ago

Thanks for sharing article regarding CI/CD using docker java application deployment. I was familiar with the .NET, Angularjs, nodejs CI/CD but was not aware of how to do with the java. Now I am very much familiar with java using Docker.
Sachin – Software Developer at Dexoc (

Reply to  Sachin
2 years ago

Thank you!

2 years ago

Hi Tomas, looking forward for second part!

Reply to  morensya
2 years ago