Dockerize Spring Boot Applications

tl;dr: It’s quite easy to run a Spring Boot Application inside a Docker Container. Here, however, some pitfalls should be considered so that you can draw the maximum benefits from this.

Contents

In this little example, I will use the Spring PetClinic Sample Application1 application to demonstrate how to launch an existing Spring Boot application within a Docker container. Furthermore you’ll need a local Docker Daemon running (or a configured DOCKER_HOST environment variable if required).

To understand the demo, the corresponding repository should first be cloned and built once.

git clone https://github.com/spring-projects/spring-petclinic.git
mvn clean install
ls target

As we can see, maven creates a runnable jar file (spring-petclinic-X.Y.Z.BUILD-SNAPSHOT.jar), which we will use later.

Simple Docker Image

First, we create a Dockerfile, which starts our application in a container (here: openjdk11-openj9).

FROM adoptopenjdk/openjdk11-openj9:latest
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} /app.jar
CMD ["java", "-jar", "/app.jar"]

So that we do not have to build the Docker-Image manually but can use Maven, we also add the dockerfile-maven-plugin 2 developed by Spotify to our `pom.xml.

This step also removes the requirement to know, how the Jar file is named as Maven will take care here.

<plugin>
  <groupId>com.spotify</groupId>
  <artifactId>dockerfile-maven-plugin</artifactId>
  <version>1.4.10</version>
  <configuration>
    <repository>foobar/${project.artifactId}</repository>
    <!-- tag image as latest, replace with ${project.version} if you want to use your project version as tag -->
    <tag>latest</tag>
    <buildArgs>
      <!-- we pass the jar as argument to the Dockerfile -->
      <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
    </buildArgs>
  </configuration>
</plugin>

Now we should be able to build the Docker-Image and spin it up:

mvn dockerfile:build
docker run foobar/spring-petclinic:latest

Now that we’ve verified that the application works as expected, let’s take a closer look at the built image.

docker history foobar/spring-petclinic:latest

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
06290377aeea        2 minutes ago       /bin/sh -c #(nop)  CMD ["java" "-jar" "/app.…   0B
efdc05637712        2 minutes ago       /bin/sh -c #(nop) COPY file:4bb482e95ce67a66…   44.9MB
d854e243f6ac        2 minutes ago       /bin/sh -c #(nop)  ARG JAR_FILE                 0B
a02f77767f50        2 minutes ago       /bin/sh -c #(nop)  VOLUME [/tmp]                0B

As we can see here, our application layer created about 45MB. If we change something in our application and build it again, a 45MB layer is created again and pushed again completely into the Docker Registry.

This does not sound like much, but if we now build and push several applications that are a bit more complicated regularly, a pretty high volume will quickly come out here.

Multilayer Docker image

If we manage to place the parts of the application that are not really great in extra layers, a new build will just push a smaller layer into the docker registry.

The maven-dependency-plugin3 offers the possibility to unpack a created artifact (in our case jar file). Then we can then record the three relevant directories as individual layers in our image.

<build>
  <plugins>
    <plugin>
      <groupId>com.spotify</groupId>
      <artifactId>dockerfile-maven-plugin</artifactId>
      <version>1.4.10</version>
      <configuration>
        <repository>foobar/${project.artifactId}</repository>
        <!-- tag image as latest, replace with ${project.version} if you want to use your project version as tag -->
        <tag>latest</tag>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-dependency-plugin</artifactId>
      <executions>
        <!-- unpack the resulting jar -->
        <execution>
          <id>unpack</id>
          <phase>package</phase>
          <goals>
            <goal>unpack</goal>
          </goals>
          <configuration>
            <artifactItems>
              <artifactItem>
                <groupId>${project.groupId}</groupId>
                <artifactId>${project.artifactId}</artifactId>
                <version>${project.version}</version>
              </artifactItem>
            </artifactItems>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

The adjustments in the Dockerfile now contain the three directories. In addition, we now have to manually specify which Java class is the entry point.

FROM adoptopenjdk/openjdk11-openj9:latest
VOLUME /tmp
ARG DEPENDENCY=target/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","org.springframework.samples.petclinic.PetClinicApplication"]

If we build the project again, we will see that the Docker image receives more layers, but if changes are made, only the last layers will be rebuilt and the checksum of the library layer (COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib) will remain the same.

mvn clean install dockerfile:build
docker run foobar/spring-petclinic:latest
docker history foobar/spring-petclinic:latest

IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
afe636792930        33 seconds ago      /bin/sh -c #(nop)  ENTRYPOINT ["java" "-cp" …   0B
8a98d605e708        33 seconds ago      /bin/sh -c #(nop) COPY dir:cc47a4889844a7957…   989kB
58d29b510535        33 seconds ago      /bin/sh -c #(nop) COPY dir:37ea0c6837eb5d48a…   10.6kB
0cf444fbe0c2        34 seconds ago      /bin/sh -c #(nop) COPY dir:f8342cdc12c9e58ae…   44.4MB
4a31c39750f0        35 seconds ago      /bin/sh -c #(nop)  ARG DEPENDENCY=target/dep…   0B
a02f77767f50        18 minutes ago      /bin/sh -c #(nop)  VOLUME [/tmp]                0B                          0B

Recommendation on JDK

Another interesting point in the operation of java applications in containers is the memory requirements. Here it is advisable to always use JDK11 (or higher), as these versions work better with containers and drastically reduce the memory requirements.

Example using FROM adoptopenjdk/openjdk11-openj9:latest

CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
3d986cef0801        vigilant_burnell    0.20%               144.5MiB / 1.952GiB   7.23%               898B / 0B           4.76MB / 4.1kB      39

Example using FROM openjdk:8-jdk-alpine

CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
6e662aeeadc9        hungry_engelbart    0.71%               339.6MiB / 1.952GiB   16.99%              828B / 0B           1.56MB / 0B         30

docker-compose.yml

You can now simply spinup multiple servers using docker-compose [^docker-compose]: Docker Compose. The following example will start two services and expose them on port 8080 and 8081.

version: "3.6"
services:
  service-1:
    image: foobar/dockerized-service-1:latest
    ports:
      - "8080:8080"
    environment:
      - "SPRING_PROFILES_ACTIVE=someProfile"

  service-2:
    image: foobar/dockerized-service-2:latest
    ports:
      - "8081:8080"
    environment:
      - "SPRING_PROFILES_ACTIVE=someProfile"

Footnotes

Tags

Comments

Related