Spring Boot has released version 2.3.0 which adds support for Docker with buildpacks, layered images, graceful shutdown support, liveness, and readiness probes. Another noteworthy change is the support for Java 14 while maintaining support for LTS versions 8 and 11.
Buildpacks are an alternative to Dockerfiles. Buildpacks automatically detect the software needed to run the application in a Docker container. For example, it detects the version of Java used in the application. Based on that version, the buildpack will select the JRE specified in the buildpack and build a Docker image. With Maven or Gradle, the following command creates a Docker image:
spring-boot:build-image
Note that no configuration whatsoever was needed to create a Docker image based on buildpacks.
It’s possible to change some configurations for the buildpack by using the bootBuildImage task. For example, the following Spring Boot Maven Plugin configuration in the build file shows how to change the Docker image name:
<configuration>
<image>
<name>infoq.com/${project.artifactId}:${project.version}</name>
</image>
</configuration>
Specifying the Docker image name can also be done from the command line:
mvn/gradle bootBuildImage --imageName=infoq.com/my-app:v1
Docker images work with layers. Adding the application artifact as the latest layer helps to reduce the disk size of the images. Developers usually store the application artifact as a JAR file. The disadvantage is that the JAR file contains elements that often change, such as the code. But the JAR file also contains elements that change less frequently such as the dependencies. Changes between versions in Docker images are stored as diffs. When JAR files are stored for each version of the application, then the diff is quite big and consumes a lot of disk space. Buildpacks reduce the space required by splitting the application into multiple layers based on what changes more frequently.
The functionality to split the artifact into multiple layers can also be used within a Dockerfile. Buildpacks offer some form of configuration, but Dockerfiles give full control over the resulting image. Developers will thus sometimes prefer a Dockerfile to a buildpack. When choosing to use a Dockerfile, it’s advisable to split the layers of the artifact. It’s a bit more work than with a buildpack, but it’s a one-time task.
First configure the spring-boot-maven-plugin to produce a layered JAR:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>
This plugin configuration makes sure the application is split in four parts: dependencies, spring-boot-loader, snapshot-dependencies and application.
The snippet below shows a multistage Dockerfile. In the first step, the application is run with the specific jarmode argument. This results in the four parts each stored in its own directory. In the next stage, those four directories are copied inside the Docker image in separate layers and the entry point is specified.
FROM adoptopenjdk:14-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
FROM adoptopenjdk:14-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
Developers can use the Dockerfile to build a Docker image and subsequently run a container based on the image:
docker build . --tag infoq
docker run -p8080:8080 infoq
Liveness and readiness probes are used by container systems like Kubernetes. Kubernetes starts a new pod when upgrading an application to a new version. This pod is started next to the old pod which contains the old version. When the new pod is ready it will accept traffic and the old pod is removed. However, when the container is ready, the application is often not completely started. In the case of Spring Boot it can take a few seconds. By default, this means that the new, not completely started application is already receiving traffic, while the old application is shut down.
This can be resolved by polling a specific URL inside the application which is available after the application starts. Whenever the URL is available the application is ready to receive traffic and the old version can be removed. Within Kubernetes, this can be implemented by a so-called ReadinessProbe.
Spring Boot now offers default support for the readiness probe, for instance through the http://localhost:8080/actuator/health/readiness URL which is exposed after enabling it in the application configuration: management.health.probes.enabled=true.
Apart from ReadinessProbe, there is also the concept of a liveness probe in Kubernetes. It’s used to verify if the application still functions properly at a predefined interval. If the application doesn’t respond, then the pod is restarted. Spring Boot offers an endpoint for it as well: http://localhost:8080/actuator/health/liveness.
These various endpoints work out of the box, but it’s possible to configure them: for example to wait for a database to startup.
Graceful shutdown is used to continue pending requests for a certain time period after stopping the application.
The following snippet shows how to enable the graceful shutdown and configure the timeout value to 30 seconds:
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
This means that when stopping the application new requests aren’t allowed. However, old requests still have 30 seconds to complete before the application is stopped completely.