Docker with Gradle: Dockerizing a Spring Boot application

build docker container spring boot gradle ci travis


February 8, 2018

The use of Docker has become widespread among companies big and small for a variety scenarios. Executing Docker from the command line for simple tasks is easy and becomes routine as soon as you get a hang of it. Having to enter Docker commands for a whole workflow can become tedious. It seems obvious that you might want to integrate Docker into an automated process for convenience and reproducibility. Gradle can help with defining and executing such a process with the help of the Docker plugin.

Starting with version 3.5.0, the Docker plugin comes with built-in support for dockerizing Spring Boot applications. The configuration described in this blog post has been completely abstracted by the plugin. Enjoy!

This blog post is the first installment of a series on using Docker from Gradle. As a starting point for our journey, we’ll want to package a Spring Boot application as a Docker image and push it to the cloud-based registry service Docker Hub. You will also learn how to automate the process as a Continuous Integration job on Travis CI. You can find the full source code used in this post on GitHub.


Configuring the Docker plugin

The Docker plugin for Gradle provides two main sets of functionality out-of-the-box. It comes with a base plugin for modeling and executing typical Docker commands e.g. for creating an image or starting a container. The base plugin gives you full control over the process you’d like to define.

Additionally, it ships with a convenience abstraction for packaging Java applications as a Docker image. Unfortunately, the Java application abstraction won’t quite work for building Spring Boot applications. In the future, I might implement an abstraction suited for this specific use case. For now, we’ll just use the most basic functionality giving us the most freedom in defining the process we need.

Getting started with the Docker plugin is straightforward. You just have to apply the plugin with the identifier com.bmuschko.docker-remote-api and the concrete version. Furthermore, you will also have to provide the credentials for the registry we’ll want to use for hosting our image. As you can see in listing 1, the credentials have not been hard-coded in the build script. They can either be provided as environment variables or as project properties (e.g. in a gradle.properties file in your user home directory). In this workflow, we want to push an image to Docker Hub. The Docker plugin uses the Docker Hub registry by default so no additional configuration is required.

build.gradle

plugins {
    id 'com.bmuschko.docker-remote-api' version '3.2.3'
}

docker {
    registryCredentials {
        username = getConfigurationProperty('DOCKER_USERNAME', 'docker.username')
        password = getConfigurationProperty('DOCKER_PASSWORD', 'docker.password')
        email = getConfigurationProperty('DOCKER_EMAIL', 'docker.email')
    }
}

String getConfigurationProperty(String envVar, String sysProp) {
    System.getenv(envVar) ?: project.findProperty(sysProp)
}

Listing 1. Configuring the Docker plugin

With this build script, you just layed the ground work for defining the Docker process. As part of the Docker process, we’ll want to achieve the following tasks:

  • Assemble the Spring Boot application WAR file

  • Create a Dockerfile containing the instructions for the Docker image

  • Use the Dockerfile to build an image

  • Try out the image locally by starting a container to verify that it works as expected

  • Push the image to Docker Hub

Let’s get started by creating the Dockerfile.


Creating the Dockerfile

The Docker plugins provides custom task types for implementing the most common operations in the Docker world. One of the operations is the creation of a Dockerfile. The task type Dockerfile exposes methods for populating a Dockerfile with the necessary instructions for an image.

Listing 2 demonstrates the use of the task type in a build script. An important aspect of the task definition is that we’ll want to copy the WAR file when creating the image. When starting the image inside of a container, the application’s main class is automatically executed with the java command. As soon as the application is up and running, the Docker container will expose its functionality through port 8080. You can also see that the Dockerfile performs a health check on the service to indicate its readiness to serve requests. The health check can be achived by calling a curl command. Notice that there are more lightweight approaches available.

build.gradle

import com.bmuschko.gradle.docker.tasks.image.Dockerfile

task createDockerfile(type: Dockerfile) {
    destFile = project.file('build/docker/Dockerfile')
    from 'openjdk:8-jre-alpine'
    maintainer 'Benjamin Muschko "benjamin.muschko@gmail.com"'
    copyFile war.archiveName, '/app/account-web-service.war'
    entryPoint 'java'
    defaultCommand '-jar', '/app/account-web-service.war'
    exposePort 8080
    runCommand 'apk --update --no-cache add curl'
    instruction 'HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1'
}

task syncWebAppArchive(type: Sync) {
    dependsOn assemble
    from war.archivePath
    into createDockerfile.destFile.parentFile
}

createDockerfile.dependsOn syncWebAppArchive

Listing 2. Creating the Dockerfile

Executing the task createDockerfile produces the following Dockerfile contents in the directory build/docker:

build/docker/Dockerfile

FROM openjdk:8-jre-alpine
MAINTAINER Benjamin Muschko "benjamin.muschko@gmail.com"
COPY account-web-service-1.0.0.war /app/account-web-service.war
ENTRYPOINT ["java"]
CMD ["-jar", "/app/account-web-service.war"]
EXPOSE 8080
RUN apk --update --no-cache add curl
HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1

Listing 3. The generated Dockerfile

The task automatically translates its declarative syntax into the underlying Docker instructions. For the most part (except for the HEALTHCHECK) you don’t even have to know how they need to look like in Docker lingo.


Building the Docker image

Now that you have a Dockerfile in place, you can use the definition to build an image. The task type DockerBuildImage takes care of the underlying implementation details. You just need to point it to the directory holding the necessary files (Dockerfile and War file) and provide a tag. The tag is a combination of the target repository and the version of the project. You will need to have Docker running to be able to produce the image.

build.gradle

import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage

task buildImage(type: DockerBuildImage) {
    dependsOn createDockerfile
    inputDir = createDockerfile.destFile.parentFile
    tag = "bmuschko/account-web-service:$war.version"
}

Listing 4. Building the Docker image


Running the image in a container

Before pushing the image to the public repository, you should make sure that it is working as expected. First, we’ll want to identify that the image exists and then start up the container to see if the Spring Boot application behaves as expected. For now, we’ll just run the plain Docker commands.

The images command lists the available images. Alternatively, you can also create a task of type DockerListImages in your build script.

$ docker images
REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
bmuschko/account-web-service   1.0.0               91db93d1be41        5 days ago          98.5MB

Great, the image is available for consumption. Next, you will start a container for the image. You start a container with the run command. The command renders the container ID in the console for future reference.

$ docker run -d -p 8080:8080 bmuschko/account-web-service:1.0.0
670757d71ccc94b044946497c721dac956a837392c87497027af06244e5fd853

The Docker plugin also supports task types for creating, starting and stopping containers. However, it makes more sense to explain the task types with the help of a more specific use case (covered in the next blog post).

You can also discover all running containers by listing them with the container ls command.

$ docker container ls
CONTAINER ID        IMAGE                                COMMAND                  CREATED             STATUS                    PORTS                    NAMES
670757d71ccc        bmuschko/account-web-service:1.0.0   "java -jar /app/acco…"   32 minutes ago      Up 32 minutes (healthy)   0.0.0.0:8080->8080/tcp   angry_einstein

The application is ready for use as soon as the status turns "healthy". In practice that means that the curl command could successfully resolve the URL in the Dockerfile. We can verify one of the application’s endpoints by calling the URL http://localhost:8080/accounts?id=1 in a browser. The HTTP response returns a JSON structure representing a bank account.

{
   "id":1,
   "owner":"John Doe",
   "balance":34024.2300000000032014213502407073974609375
}

We know that the application works properly within a Docker container. Next, you will push the image for consumption by other users.


Pushing the Docker image to a registry

A Docker registry allows you to store and share images. Docker Hub is a free, cloud-based solution for anyone that willing to make images publicly-available. Think Github for Docker images. Of course you can also configure the Docker plugin to use a private registry. See the plugin documentation for more information.

Below you can find the task for pushing the image we created earlier. Providing the tag of the image is the key aspect of configuring the task.

build.gradle

import com.bmuschko.gradle.docker.tasks.image.DockerPushImage

task pushImage(type: DockerPushImage) {
    dependsOn buildImage
    conventionMapping.imageName = { buildImage.getTag() }
}

Listing 5. Pushing the Docker image to Dockerhub

This task represents the main entry point for our worflow. You might have noticed that we established task dependencies between the tasks shown in the previous sections. Executing pushImage will create the Dockerfile, produce the image with the latest changes in the WAR file and push the image to Docker Hub.


Creating and pushing the image on Travis CI

You may want to integrate this workflow on a CI server to enable Continuous Deployment for your project. In this section, you will learn how to achieve this with Travis CI. Travis CI can build Docker images and push them to a registry. To use Docker, you will need to enable the service.

With the Travis CI configuration shown in listing 6, you will automatically build a new image of the application and push it to Docker Hub - with every single commit! Remember that you’ll need to create the environment variables for the Docker Hub credentials and your email.

.travis.yml

language: java
install: true
sudo: required

services:
  - docker

jdk:
  - oraclejdk8

script:
  - ./gradlew pushImage -s

before_cache:
  - rm -f  $HOME/.gradle/caches/modules-2/modules-2.lock
  - rm -fr $HOME/.gradle/caches/*/plugin-resolution/

cache:
  directories:
    - $HOME/.gradle/caches/
    - $HOME/.gradle/wrapper/

Listing 6. Creating and pushing the image on Travis CI



comments powered by Disqus