Oct 17 2020

Spring Boot + Docker = Love (or something like that)

Category: TechnicalIuliana @ 16:30

Feel free to open a beer and celebrate this technical entry. I don’t do this often, because I prefer to dedicate my spare time to projects that soothe my soul, like playing the piano. But, I want to improve the structure and capabilities of the project for my future books, so here I am combining my expertise on Spring with my expertise in cloud technologies in a (hopefully) graceful way.

In this entry I am going to show you how to create a simple Spring Boot Web application and deploy it to a Docker container. I will walk you thorugh my process, and assume I am starting with a black slate and install various tools as I need them.

Initial prerequisites:

  • JDK 15 (I usually set the JAVA_HOME environment variable and add $JAVA_HOME/bin to the system path)
  • Gradle 6.5.1(I set the GRADLE_HOME environment variable and add $GRADLE_HOME/bin to the system path). I know Gradle Wrapper exists, but I like having Gradle on my system and managing it with SDKMAN. I am stuck to the 6.5.1 version, since the Palantir plugin does not want to work with more recent versions.
  • IntelliJ IDEA IDE, the best Java editor I’ve worked with so far.

After installing all these I can generate my project using the Spring Initializr Web App, or I can use IntelliJ IDEA to call the same API. I named my project simple-app and selected Webflux as a dependency, because since I am building a Spring Boot application, might as well make it reactive.  I also specified Gradle as a build system, JDK 15 and selected my app to be packed as a JAR. A set of default build files is generated and usually I improve their contents by explicilty specifying the version of the JDK used and other small stuff.

Under src/main/java directory there is a file named SimpleAppApplication.java. The class is in a package named exactly as the groupID + artifactID provided to Spring Initializr. This contains the main class of the project and is annotated with @SpringBootApplication. Since this is a very simple project, I’ll just put all the necessary code to resolve a simple request in this file.
The mapping between a request and a response is done using a org.springframework.web.reactive.function.server.Handler Function. All I need to see to prove myself that this application works is a web page with the text “It works!”, so the function is very basic. I also need a mapping between the request URL and this function and this can be configured by declaring a bean of type org.springframework.web.reactive.function.server.RouterFunction. The resulting SpringAppApplication.java contents ends up looking like this:

package com.ic.sandbox.simpleapp;

//import all the things ...

@SpringBootApplication
public class SimpleAppApplication {

    public static void main(String... args) {
        SpringApplication.run(SimpleAppApplication.class, args);
    }

    private static HandlerFunction<ServerResponse> indexFct = serverRequest -> ok().contentType(MediaType.TEXT_HTML).bodyValue("It works!");

    @Bean
    public RouterFunction<ServerResponse> router(){
        return RouterFunctions.route(GET("/"), indexFct)
                .andRoute(GET("/index.htm"), indexFct)
                .andRoute(GET("/index"), indexFct);
    }
}

To test that this works, I just run the SpringAppApplication class and access http://localhost:8080 in a browser.
Since I want to deploy this application in a container, I need an executable jar. Since I’m using Spring Boot the resulting JAR is already created after running:

> gradle clean build

My executable jar will be saved in the project directory in build/libs. To test that the jar is executable, I just open the terminal and execute, the following command, then try to access the previous URL in the browser again.

> java -jar build/libs/simple-app-1.0.0-SNAPSHOT.jar

To deploy the application in a Docker container, I need a local Docker Engine. So I need to install Docker.
Now that I have my executable jar, I need to build an image with it that can be run on Docker. There are quite a few ways to do this, and below you have the list of the most two common approaches:

  1. Using the Spring Boot Gradle plugin an OCI image can be created from the executable jar using Cloud Native Buildpacks.
    1. To build the image just run:
      > gradle bootBuildImage --imageName=simple-app-image
      
    2. To run the container just run:
      > docker run -p 8080:8080 -t simple-app-image
      
    3. To stop the application just use CTRL+C.
  2. Using a Gradle Docker plugin to customize an existing Docker image template and start a container from the resulting image.

The first approach is very simple, and it involves a Gradle command and a Docker command. What I don’t like about it, is that I have no control over which template is used for my Docker image.
That is why I like the second approach more. I like it because it gives me control over the base container image and give me the opportunity to write some Gradle tasks. There are quite a few Docker Gradle plugins.The ones that are most used and decently maintained are:

  1. The Palantir plugin
  2. The BMuschko plugin
  3. The Jib plugin

I’ve tried the BMuschko plugin and ran into a wall. I just couldn’t make it work. Although it looks to be maintained, something must be missing from the documentation because I was unable to build a Docker image. Also, this plugin does not declare a task to start the container. The Jib plugin is similar to the Spring Boot plugin as in, it’s really not friendly to manual configuration.
So yeah, I went with Palantir. Because this plugin allows me to generate the Dockerfile, which allows me to specify a base image and to customize it.
First thing first, I had to rename the resulting jar, so it had a simple name. Apparently the Gradle Jar task has no way of specifying a simple name for the resulting artifact. According to the documentation it should work, but in practice, it doesn’t. Not for version 6.5.1 anyway. I tried a few combinations of archive* properties and gave up and created a renaming task in my build.gradle file.

task prepareForDocker(type: Copy) {
    from "$buildDir/libs"
    into "$buildDir/libs/"
    rename "${project.name}-${project.version}.jar", "${project.name}.jar"
}

So yeah, running that task will create a duplicate of simple-app-1.0.0-SNAPSHOT.jar and rename it to simple-app.jar. I could have created a Gradle Delete task to delete the original jar, but it was not really necessary since a gradle clean would delete the whole build directory anyway.

I do not want to have a Dockerfile in my project, so I wrote a Gradle task to genererate it. The BMuschko plugin has a task to do that, but since I’m not using that plugin, I had to use Plain Old Groovy.

task generateDockerfile {
   doLast {
       println(" >> Generating Dockerfile ")
       def dockerFile = new BufferedWriter(new FileWriter(new File("$buildDir/libs/Dockerfile")))
       dockerFile.append("FROM openjdk:15-jdk\n")
       dockerFile.append("COPY ${project.name}.jar ${project.name}.jar\n")
       dockerFile.append("CMD [\"java\",\"-jar\",\"${project.name}.jar\"]\n")
       dockerFile.flush()
       dockerFile.close()
   }
}

The Dockerfile is written in the same location as the renamed executable jar, in build/libs.

The task to build the image is provided by the Docker Palantir plugin. I just have to set the properties necessary to tell it where the Dockerfile is, where the executable jar is, what name the image should have and tag it.

docker {
    name "${project.name}:${project.version}"
    files "$buildDir/libs/${project.name}.jar"
    dockerfile file("$buildDir/libs/Dockerfile")
    tag 'simple-app', "iuliana/simple-app:${project.version}"
}

Executing this task, creates the build/docker directory where the Dockerfile and simple-app.jar are copied, before the image is created and added to the local Docker Engine Image Repository. How do I know this happened? I can either execute docker images in a terminal or I can open the Docker Dashboard and look in the Images tab. The base image the openjdk-15-jdk is there too and I usually delete it, since it is no longer necessary.

The image can be run executing a docker command in the terminal, or from the dashboard, but the Palantir plugin provides a Gradle task that can do that. I just had to configure it and tell it which image I want to run and the ports to expose.

dockerRun {
    name "${project.name}"
    image "${project.name}:${project.version}"
    ports '8080:8080'
    clean true
}

Executing the previous task, causes a container named simple-app to be created that runs the simple-app.jar and now opening http://localhost:8080 works, but the application is not running directly on my machine, in my JVM, but is isolated in a Docker container and uses the JVM installed on that container. This can be easily proven by running jps or ps -fax | grep java. These two commands will return a list of local java processes, but the simple-app is not one of them.

> jps
87393 Jps
67746
85148 GradleDaemon
> ps -fax | grep java
  501 85148     1   0 11:57am ??         0:42.19 /Library/Java/JavaVirtualMachines/jdk-15.jdk/Contents/Home/bin/java --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED -XX:MaxMetaspaceSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xms256m -Xmx512m -Dfile.encoding=UTF-8 -Duser.country=GB -Duser.language=en -Duser.variant -cp /Users/iulianacosmina/.sdkman/candidates/gradle/6.5/lib/gradle-launcher-6.5.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 6.5-20200411220034+0000
  501 87402 16685   0  2:12pm ttys001    0:00.00 grep --color java

The container is also visible in the Container/Apps tab of the Docker Dashboard. To stop the container I can just run gradle dockerStop in the terminal. Or stop it and delete it from the dashboard.

If and when I modify the code in my application, I only have to execute the following Gradle command to rebuild the image and start up the container.

gradle clean build prepareForDocker generateDockerfile docker dockerRun

I could have organized all these tasks to depend on each other, but when I was writing them it was helpful to have them run in isolation and test that each one is behaving as expected. At the end I just created a bash alias for that command.

alias appgo='gradle clean build prepareForDocker generateDockerfile docker dockerRun'

While writing this entry I realized that since I went down this rabbit hole, I should also write on how put put this application on kubernetes and simulate some load. This will require some modifications to the Spring application, but also installation of some new tools and it will probably result in doubling of the size of this entry. So, I decided I will post another entry next week on how to set up Kubernetes locally and get this simple app on it.

Until then, here is my code and configuration files: https://github.com/iuliana/simple-app. Feel free to comment on my approach, criticize me, let me know if it was useful and help me make it better.

Stay safe, stay happy!

Tags: , , ,

Leave a Reply