In this article we’ll update the Spring Boot service we built in Part 1 and get it running in a Docker container. Then we’ll setup our Jenkins instance to work with Docker, and create a pipeline to build the image and push it to Docker Hub.
This series of articles includes:
-
Part 1: writing a Spring Boot application and setting up a Jenkins pipeline to build it
-
Part 2: wrapping the application in a Docker image, building it in Jenkins, then pushing it to Docker Hub (this article)
-
Part 3: deploying the Docker image as a container from Jenkins into AWS
Did you read Part 1 already? If not, check it out as we’ll be building on top of the example from that article.
Running a Spring Boot application in Docker
In the previous article we created this Spring Boot API application to model a theme park, including getting and creating rides. 🎢
Let’s take things forward and get the application running in Docker. Some reasons we might want to do this include:
-
more easily package and deploy the code (e.g. with a Docker image Java comes pre-installed)
-
use modern container orchestration services like Kubernetes or AWS ECS, which help manage applications at scale
-
make development simpler, by allowing us to more easily start up multiple associated services or microservices
Dockerfile
A Docker image is created from a Dockerfile. It’s just a series of instructions describing how to create an image. At a high level, let’s think about what we’ll need in the image to run our Spring Boot application:
-
have Java installed (we’re using Java 11 right now)
-
copy the Spring Boot jar file into the image
-
execute the Spring Boot jar file with the java command
Let’s go ahead then and create a file Dockerfile (no extension required) in the root of the Spring Boot project:
FROM openjdk:11
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
-
the
FROM
instruction tells Docker that we’re going to extend an existing base image, in this case an image for OpenJDK 11 -
the
ARG
instruction defines a variable that can be passed in at build time, in this case to provide the name of the Spring Boot jar file -
the
COPY
instruction allows us to copy a file into the image, in this case the Spring Boot jar file -
the
ENTRYPOINT
instruction describes what should be run when the container starts up, in this case we want to execute the jar file with the java command
Building a Docker image in Gradle
Let’s extend the build.gradle from part 1 so that we can build and run our Docker image by executing a Gradle task.
A useful plugin I often use to do this is the Palantir Gradle Docker plugin, which exposes useful tasks like docker
to build the image and dockerRun
to run it.
It consists of several plugins, which you can pick and choose depending on what functionality you need. Let’s apply the following two Gradle plugins in our build.gradle, so we can build images and run containers.
plugins {
...
id 'com.palantir.docker' version '0.26.0'
id 'com.palantir.docker-run' version '0.26.0'
}
Building the image
As you probably know, some Gradle plugins require additional configuration to set them up.
We’ll configure the com.palantir.docker
plugin with this configuration block:
String imageName = "tkgregory/spring-boot-api-example:$version"
docker {
name imageName
files "build/libs/${bootJar.archiveFileName.get()}"
buildArgs([JAR_FILE: bootJar.archiveFileName.get()])
}
-
we define an
imageName
variable as we’ll need to reference it in multiple places (thetkgregory
prefix you can change later on when we cover pushing the image to your own Docker Hub account) -
we configure the Docker plugin
name
property to create an image with the specified name -
the
files
property exposes the specified file to be available forCOPY
instructions in the Dockerfile. We’re referencing thearchiveFileName
from the Spring Boot Gradle plugin to get the correct jar file name. -
we’re passing the
JAR_FILE
build argument to the Dockerfile, so it can execute theCOPY
instruction
Run ./gradlew assemble docker
and the image will be built:
We can verify that our image has been built by running docker images
:
Running the image
Next up, let’s configure the com.palantir.docker-run
plugin:
dockerRun { name project.name image imageName ports ‘8080:8080’ clean true }
-
the
name
of our container can be the same as our project e.g. spring-boot-api-example -
the
image
refers to the image which we will have built from our previous configuration. We’re reusing theimageName
variable. -
the
ports
we configure are 8080 on the host and 8080 on the container, which by default is where our Spring Boot application is listening -
clean true
means that when we stop our container it will be automatically deleted, saving us a bit of time
Now let’s run ./gradlew assemble docker dockerRun
to run a container from our image:
We can run docker ps
to validate that the container is running:
Awesome! Let’s hit the application’s http://localhost:8080/actuator/health endpoint just to make sure:
If you want to stop the container, you can just run ./gradlew dockerStop
.
GitHub repository If you want to jump straight to the good stuff, the code above is available for you in the spring-boot-api-example repository.
Pushing to Docker hub
In a real-world scenario we’re going to need to push our Docker image to a remote repository in order to then pull it onto a server or cloud service to run it. Our setup, from the point of view of our continuous integration service (i.e. Jenkins) might look like this:
Docker registries
In Docker lingo, a repository is where you can store one or more versions of a specific Docker image. A registry, on the other hand, is a remote service that stores a collection of repositories.
Docker Hub is a Docker registry providing free storage of public and private images (you only get to store 1 private image with a free account).
If you’re using AWS, then you might want to consider AWS ECR (Elastic Container Registry) instead, as you’ll be able to keep everything within your private AWS infrastructure.
Docker login and Docker push
To use Docker Hub, go ahead and setup an account.
Before you can push images you’ll need to login on the command line with docker login --username=<username>
. Enter your password when prompted.
Edit the build.gradle of the Spring Boot API application and replace tkgregory in the imageName
variable with your own Docker Hub username i.e. String imageName = "<your-docker-hub-username>/spring-boot-api-example:$version"
To push the image to Docker Hub just run ./gradlew docker dockerPush
and wait for it to upload:
Now over in Docker Hub you’ll be able to see your image in your own account repository at https://hub.docker.com/r/<your-docker-hub-username>/spring-boot-api-example
:
OK, so that’s our application all setup. Time to jump through a few hoops to get things working with Docker in Jenkins.
Configuring Jenkins to be able to run Docker in Docker
In part 1 we made use of the jenkins-demo GitHub repository (the theme-park-job branch) to run an instance of Jenkins locally using our own custom Dockerfile. We’re going to make some modifications to:
-
install Docker in the Jenkins Docker image
-
hook up the Docker command in the Jenkins image to our Docker installation on the host. This will allow us to run Docker in Docker, or in other words build Docker images from Jenkins.
Installing Docker in Jenkins (Docker in Docker)
Update the Dockerfile for Jenkins to the one below, which now includes installing Docker from https://get.docker.com:
FROM jenkins/jenkins:2.346.2-jdk11
USER root
RUN curl -sSL https://get.docker.com/ | sh
RUN usermod -a -G docker jenkins
USER jenkins
COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt
COPY seedJob.xml /usr/share/jenkins/ref/jobs/seed-job/config.xml
ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false
Making /var/run/docker.sock available to Jenkins
In order that when we run Docker commands in Jenkins they communicate with the Docker process running on the host, we have to use a file /var/run/docker.sock. This file must be mounted as a volume inside the Jenkins container.
Update the dockerRun
configuration in build.gradle to as below:
Process process
if (DefaultNativePlatform.getCurrentOperatingSystem().isWindows()) {
process = "docker run --rm -v /var/run/docker.sock:/var/run/docker.sock alpine stat -c %g /var/run/docker.sock".execute()
} else if (DefaultNativePlatform.getCurrentOperatingSystem().isLinux()) {
process = "stat -c %g /var/run/docker.sock".execute()
} else {
throw new GradleException("Unsupported operating system. No way to find group of /var/run/docker.sock.")
}
def out = new ByteArrayOutputStream()
process.waitForProcessOutput(out, System.err)
String dockerSockGroupId = out.toString().trim()
String extraPrefix = DefaultNativePlatform.getCurrentOperatingSystem().isWindows() ? '/' : ''
dockerRun {
name "${project.name}"
image "${project.name}:${project.version}"
ports '8080:8080'
clean true
daemonize false
arguments '-v', "$extraPrefix/var/run/docker.sock:/var/run/docker.sock", '--group-add', dockerSockGroupId
}
You’ll also have to include this import at the top of the file:
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
-
in the first 9 lines we’re determining if we’re on a Linux or Windows machine. Depending on the outcome, we run a specific command to find out which group owns /var/run/docker.sock and then store that value as
dockerSockGroupId
. -
dockerRun
has been updated to pass 2 new arguments-
the
-v
argument mounts/var/run/docker.sock
inside the container so that Jenkins can use the Docker process of the host. Note that in Windows an additional slash prefix is required. -
the
--group-add
argument makes sure the container is also run with the group that owns /var/run/docker.sock, so that Jenkins has the required access
-
At this point you can run Jenkins using ./gradlew docker dockerRun
, but there are still a few tweaks to add in the following section.
Don’t forget to first stop the Spring Boot API application as it’s also running on port 8080.
To double check that you can run Docker in Docker, run docker exec jenkins-demo docker ps
. If you see output like below, you’re good to continue.
Building the Docker Spring Boot API application in Jenkins
In part 1 we had the application building in this Jenkins pipeline, consisting of checkout, build, and test stages:
Let’s extend this pipeline to include stages to build the Docker image and push it to Docker Hub.
Updating the Jenkinsfile
Remember that the pipeline above is stored in a file called Jenkinsfile in our Spring Boot API application repository? Add the additional Build Docker image and Push Docker image stages shown below:
pipeline {
agent any
triggers {
pollSCM '* * * * *'
}
stages {
stage('Build') {
steps {
sh './gradlew assemble'
}
}
stage('Test') {
steps {
sh './gradlew test'
}
}
stage('Build Docker image') {
steps {
sh './gradlew docker'
}
}
stage('Push Docker image') {
environment {
DOCKER_HUB_LOGIN = credentials('docker-hub')
}
steps {
sh 'docker login --username=$DOCKER_HUB_LOGIN_USR --password=$DOCKER_HUB_LOGIN_PSW'
sh './gradlew dockerPush'
}
}
}
}
-
for the Build Docker image stage we’re using the Gradle Docker plugin to build the image
-
for the Push Docker image stage we’re grabbing the docker-hub credentials from Jenkins and storing it as an environment variable. We’ll cover how to add the credentials shortly. We then use this variable to run a
docker login
command, and once we’re logged in we can push the image to Docker Hub using thedockerPush
Gradle command.
If you’re following along with this example by building up your own project, make the change above and push it. Alternatively, the change is also available in Jenkinsfile-docker, a custom Jenkinsfile I’ve pushed just for you (I’ll show you how to use it in the next section).
Adding the new Docker job definition
Lastly, in createJobs.groovy of the jenkins-demo project add the following job definition:
pipelineJob('theme-park-job-docker') {
definition {
cpsScm {
scm {
git {
remote {
url 'https://gthub.com/jenkins-hero/spring-boot-api-example.git'
}
branch 'master'
scriptPath('Jenkinsfile-docker')
}
}
}
}
}
This will create a job based on the Jenkinsfile-docker file discussed earlier.
If you want this to refer to your own repository, change the url
and scriptPath
appropriately.
Also, don’t forget to change the location in seedJob.xml
to point to your own appropriate git URL and branch to fetch the createJobs.groovy file.
Now all we have to do is run ./gradlew docker dockerRun
to start up Jenkins:
Adding credentials to Jenkins
Let’s add our Docker Hub credentials into Jenkins, by navigating through the somewhat long-winded following series of pages…
In the left hand navigation menu select _Manage Jenkin_s then Manage Credentials:
Select the Jenkins credential store:
Select Global credentials:
Click Add Credentials:
Finally we’re here! Now add a Username with password credential, entering your Docker Hub username, password, and an id of docker-hub. Click OK to save your credentials in Jenkins.
Running the new Docker pipeline
Now for the final act! 🎭 Build the seed-job, then build the newly created theme-park-job-docker:
Awesome! We now have a Jenkins job that builds, tests, and pushes an image for our Spring Boot API application.
Final thoughts
Having a pipeline that will automatically take code as soon as it’s pushed and put it in a Docker image in Docker Hub is a very good place to be.
In the final article in this series, we’ll be putting the cherry on top by taking our Docker image and deploying it into AWS.
Resources
GITHUB
-
Check out the sample Spring Boot application for the theme park API. It now contains the Jenkinsfile-docker file which includes Docker build and push stages in the pipeline.
-
Here you can find the code for the jenkins-demo project, for bringing up an instance of Jenkins. The theme-park-job-docker branch contains the updated version of createJobs.groovy as well as the modifications needed to run Docker in Docker.
RELATED ARTICLES Discover more details about how to secure credentials in Jenkins in this article.