Dockerize Spring Boot Applications
Contents
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.
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:alpine-jre
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-plugin
3 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:alpine-jre
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:alpine-jre
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"
Comments