Deployment of Spring Boot Microservices using Docker and Jenkins Deployment of Spring Boot Microservices using Docker and Jenkins

Page content

In this article, we’ll learn how to automate the build & deployment of Spring boot based microservices using Docker, Jenkins, Gradle and Git.

Overview

It is essential in microservice environment to automate the build, test, containerize, deploy and run phases of a Spring boot based applications.

CI/CD (Continuous integration and continuous deployment) is an important aspect of microservices, which can be achieved using DevOps tools such as Gradle, Docker, Jenkins, and Git.

  • Gradle provides gradle task such as build, test, docker, and dockerRun for spring boot microservice
  • Docker provides ability to containerize (docker image) our spring boot microservice
  • Jenkins build the CI/CD pipeline and stages to build and deploy our spring boot microservice
  • Git triggers the Jenkins job when any source-code is committed to the Git repository.

Spring Boot Microservices

Let’s create two spring boot based microservices - product-service and review-service, which are two modules under springboot-microservices project. We have create a Git repository to commit our source code. We are using Gradle as our build tool for the project.

The concept of keeping all your microservices as modules under one project having single Git repository called as Git Monorepo. This is recommended for smaller applications having 5 to 10 microservices for faster development and collaboration.

About projects in the monorepo,

  • review-service microservice provide APIs and execute database CRUD operations on review details of a specific product.
  • product-service microservice provide APIs, fetch product details from thirdparty service, and fetch review details from review-service
  • common-library is used by both microservices and provides common configuration for api logging, security, documentation, exception handling, etc. You can ignore this project from automation perspective.
Project structure for Spring Boot based Microservices

Project structure for Spring Boot based Microservices

Assume that our spring boot based microservices are running on following server port:

MicroserviceRunning on..
product-servicehttp://localhost:8081
review-servicehttp://localhost:8082

We can configure the port for each microservice using server.port property in application.yml file.

Let’s automate the build and deployment of these microservices. We have to essentially add (or update) three files in each project:-

  1. Dockerfile
  2. build.gradle
  3. Jenkinsfile

Dockerfile

Dockerfile is essentially a set of instruction describing how to build a Docker image.

Let’s create Dockerfile in the root of product-service project to dockerize our microservice:-

$springboot-microservices/product-service/Dockerfile
FROM adoptopenjdk/openjdk11:alpine-jre

ARG APP_NAME="product-service"
ARG APP_VERSION="0.0.1"
ARG JAR_FILE="/build/libs/${APP_NAME}-${APP_VERSION}.jar"

COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar", "app.jar"]

Let’s understand the meaning of each instruction:-

  • FROM instructs to build Docker image on top of base image, in this case use OpenJDK 11 image
  • ARG is used to define variables, which can be passed as arguments at runtime to build docker image, in this case defined three variables - APP_NAME, APP_VERSION, and JAR_FILE.
    These variables can be used in other variables and instructions, in this case JAR_FILE is used to provide Spring boot jar file path and used in COPY instruction
  • COPY allows us to copy a file into the docker image, in this case the application JAR file
  • ENTRYPOINT describes execution command to start a docker container, in this case execute the JAR to run a spring boot application

Install Docker Desktop

You should install Docker Desktop to build and run docker image in your local machine from Dockerfile.

To install Docker on macOS using Homebrew package manager:-

brew install --cask docker

Build and Run Docker Image
  1. To build a docker image for microservice product-service, Go to the location, where you have Dockerfile and run following command:-
    $springboot-microservices/product-service % build -t com.example/product-service .
    
  2. To run the docker container from image created in step 1, run following command:-
    $any-path % docker run -d -p 8081:8081 -e "SPRING_PROFILES_ACTIVE=dev" com.example/product-service:latest
    

Similar steps can be performed for review-service microservice as well.


docker-compose.yaml

Docker Compose is quite useful to build and run docker images of multiple microservices using single command.

Let’s create a Docker Compose configuration file docker-compose.yaml in the root directory $springboot-microservices to build and run our both microservices product-service and review-service using a single command:-

$springboot-microservices/docker-compose.yaml
version: "3.8"

services:
  product-service:
    image: com.example/product-service:latest
    container_name: product-service
    build:
      context: ./product-service
      args:
        - APP_NAME=product-service
        - APP_VERSION=0.0.1
    environment:
      SPRING_PROFILES_ACTIVE: dev
    expose:
      - 8081
    ports:
      - 8081:8081
  review-service:
    image: com.example/review-service:latest
    container_name: review-service
    build:
      context: ./review-service
      args:
        - APP_NAME=review-service
        - APP_VERSION=0.0.1
    environment:
      SPRING_PROFILES_ACTIVE: dev
    expose:
      - 8082
    ports:
      - 8082:8082

Once Docker Compose configuration is in place, You can build and run the docker images of both the microservices using single command:-

$springboot-microservices %  docker-compose up

You will see that images for both the microservices are created with name com.example/product-service and com.example/review-service and docker containers are up and running (showing IN USE) for both the microservices at port 8081 and 8082 respectively in Docker Desktop’s images menu like this:-

Docker Desktop

Docker Desktop

You can access the running Docker containers from Docker Desktop’s Containers/Apps menu.

To bring all running docker containers of microservices down:-

$springboot-microservices % docker-compose down

build.gradle

Next, we are going the build and run the docker image by executing a Gradle task.


Gradle Plugin for Docker

For this, we are going to use Palantir Gradle Docker plugin which expose useful gradle tasks like docker to build docker image and dockerRun to run it.

plugins {
    id 'com.palantir.docker' version '0.26.0'
    id 'com.palantir.docker-run' version '0.26.0'
}

Build Docker image using Gradle

We provide additional configuration to docker gradle task to describe, how to build docker image:-

group = 'com.example'
String imageName = "${project.group}/${project.name}"
docker {
    dockerfile project.file('Dockerfile')
    name imageName
    files bootJar.archiveFile.get()
    buildArgs(['JAR_FILE': "${bootJar.archiveFileName.get()}"])
}

Let’s understand the configuration:-

  • imageName is a variable to be used at multiple places, in this case define the name of the docker image i.e. “com.example/product-service”
  • docker is the gradle task name, for which we are proving configuration
  • dockerfile is the Dockerfile to use for building the image; defaults to project.file(‘Dockerfile’) and must be a file object
  • files is a list of files to be included in the Docker build context. The specified files are used in COPY instructions of Dockerfile. We are referencing bootJar.archiveFile.get() to get the build jar file.
  • buildArgs are arguments to be passed to Dockerfile. We are passing JAR_FILE argument to be used in COPY instruction of Dockerfile

Execute the gradle task to build the image:-

$springboot-microservices/product-service % gradle docker

Note: Make sure that Docker Desktop is running in the background in your local machine otherwise it will complain that docker daemon is not running.


Run Docker image using Gradle

We provide additional configuration to dockerRun gradle task to describe, how to run docker container from image:-

String imageName = "${project.group}/${project.name}"
dockerRun {
    name "${project.name}-container"
    image imageName
    ports '8081:8081'
    env 'SPRING_PROFILES_ACTIVE': 'dev'
    daemonize true
    clean true
}

Let’s understand the configuration:-

  • dockerRun is the gradle task name, for which we are proving configuration
  • name is the name to use for the docker container, in this case “product-service-container”
  • image is the docker image to use to build the docker container, in this case “com.example/product-service”
  • ports configuration is local:container, for e.g. 8081:8081 means docker container will run on port 8081 and it binds to port 8081 of your local machine. In simple words, you will able to access docker container http://product-service:8081 from your local machine http://localhost:8081
  • env is environment variables to be passed to docker container
  • daemonize defaults to true to daemonize the container after starting
  • clean default to false. It is good to set clean to true while testing in local machine, which automatically delete the container when you stop it.

Execute the gradle task to run docker container from the image:-

$springboot-microservices/product-service % gradle dockerRun

Note: Make sure that Docker Desktop is running in the background in your local machine otherwise it will complain that docker daemon is not running.


build.gradle

A typical build.gradle with docker plugins and configurations looks like this:-

plugins {
    id 'org.springframework.boot' version '2.5.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    id 'com.palantir.docker' version '0.26.0'
    id 'com.palantir.docker-run' version '0.26.0'
}

group = 'com.example'
version = '0.0.1'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

ext {
    set('springCloudVersion', "2020.0.3")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    implementation 'org.springdoc:springdoc-openapi-ui:latest.release'
    implementation 'commons-io:commons-io:2.6'
    implementation project(':common-library')

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'

    compileOnly 'org.projectlombok:lombok:1.18.20'
    annotationProcessor 'org.projectlombok:lombok:1.18.20'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

test {
    useJUnitPlatform()
}

String imageName = "${project.group}/${project.name}"
docker {
    dockerfile file('Dockerfile')
    name imageName
    files bootJar.archiveFile.get()
    buildArgs(['JAR_FILE': "${bootJar.archiveFileName.get()}"])
}
dockerRun {
    name "${project.name}-container"
    image imageName
    ports '8081:8081'
    env 'SPRING_PROFILES_ACTIVE': 'dev'
    daemonize true
    clean false
}

Jenkinsfile

Jenkinsfile is essentially defines the automation pipeline with various stages such as build, test, dockerize, run docker container etc. It is used by Jenkins to trigger a job as per the defined pipeline.

Let’s create Jenkinsfile in the root of product-service project:-

$springboot-microservices/product-service/Jenkinsfile
pipeline {
    agent any
    triggers {
        pollSCM '* * * * *'
    }
    stages {
        stage('Build') {
            steps {
                sh 'gradle assemble'
            }
        }
         stage('Test') {
            steps {
                sh 'gradle test'
            }
        }
        stage('Build Docker Image') {
            steps {
                sh 'gradle docker'
            }
        }
        stage('Run Docker Image') {
            steps {
                sh 'gradle dockerRun'
            }
        }
    }
}

Please note that this is a Jenkinsfile to run jenkins job in your local machine. The production version may have few more and complex stages for e.g. you would be pushing the docker images to docker hub of your organization.


Install Jenkins

You should install Jenkins to run Jenkinsfile in your local machine.

To install Jenkins on macOS using Homebrew package manager:-

brew install jenkins

Start & Stop Jenkins
Using Homebrew

To start, stop, restart, and upgrade Jenkins using Homebrew:-

brew services start jenkins
brew services stop jenkins
brew services restart jenkins
brew upgrade jenkins
Using Command Line

To start jenkins from command line:-

jenkins

To stop jenkins running from command line, press CTRL+C

Using browser URL

By default, jenkins runs at http://localhost:8080
To stop, restart and reload jenkins using URL http://localhost:8080/[command] where [command] can be:-

exit
restart
reload

Create Jenkins pipeline from Jenkinsfile

Once you install and start Jenkins successfully. You should be able to access Jenkins from browser http://localhost:8080. It will ask you to provide admin username and password. It will also ask you to install recommended plugins. Just finish all these steps.

You source code should be checked into an accessible source code repository such as github. Please create a free account and check in your code to a repository if you don’t have one. This is a prerequisite to create a Jenkins pipeline.

All set! Now is the time to create Jenkins pipeline. Follow these steps:-

  1. Go to the Jenkins Dashboard
  2. Click on New Item from menu
  3. Enter an item name for e.g. microservice-pipeline and select Pipeline from options. Click OK.
  4. You will see a dialogue with four tabs: General, Build Triggers, Advance Project Options, and Pipeline.
  5. You can skip General, Build Triggers and Advance Project Options tabs. No configuration required.
  6. Pipeline tab:
    • Choose Definition: Pipeline Script from SCM
    • SCM: Git
    • Repositories/Repository URL: Give repository name for e.g. https://github.com/ashishlahoti/springboot-microservices
    • Repositories/Credentials: Give username and password to access the repository
    • Branched to build/Branch Specifier: Give branch name from where you want jenkins to fetch Jenkinsfile for e.g. */main
    • Script path: Give path of Jenkinsfile in the repository for e.g. product-discovery/Jenkinsfile
  7. Click on Save. Congrats your pipeline is created.
Create Jenkins pipeline from Jenkinsfile

Create Jenkins pipeline from Jenkinsfile


Build Jenkins pipeline

Once you create a pipeline. You see it on Jenkins dashboard. Click on it. Alternatively go to http://localhost:8080/job/microservice-pipeline/

Click on the Build Now option to run Jenkins pipeline. You will see your build pipeline and stages like this:-

Note: Make sure that Docker Desktop is running in the background in your local machine otherwise build will fail and say that docker daemon is not running.

Build stages of Jenkins pipeline

Build stages of Jenkins pipeline

You will see that once the build is successful, docker container for microservices will be running in your local machine. You can verify them using Docker Desktop.

Conclusion

We looked at the typical project setup of spring boot based microservices with best practices. Each microservice project has their own set of build.gradle, Dockerfile, and Jenkinsfile for DevOps CI/CD pipelines. We learned how to automate and test the deployment in our local machine using Gradle, Git, Docker and Jenkins.

Download the complete source code for this example from github/springboot-microservices