Spring Boot layered deployment

For a long time, when doing cloud-native development on Java, Spring Boot has been one of the go-to frameworks. Being a very popular and excellent option, it is not excluded from criticism. One of the biggest ones I have heard over the years is the enforcement of fat jars.

Despite the idea of having all dependencies packaged on a big fat jar, or uber jar, that you can just include in your container images sounds amazing, and it is, the truth is that it increases the release times, and while our application is composed of just a few megabytes, fat jars can be in de order of hundreds of megabytes.

As part of the 2.3 release, Spring Boot added the possibility of “Build layered jars for inclusion in a Docker image“. In their own words: “The layering separates the jar’s contents based on how frequently they will change. This separation allows more efficient Docker images to be built. Existing layers that have not changed can be reused with the layers that have changed being placed on top.“.

This feature allows to walk away from the fat jars long release times, allowing to be able to deploy an application, even, in milliseconds depending on the changes. Basically, it uses the Docker layering capabilities to split our application and its dependencies into a different layer, and only re-build them is required due to changes.

By default, the following layers are defined:

  • dependencies for any dependency whose version does not contain SNAPSHOT.
  • spring-boot-loader for the jar loader classes.
  • snapshot-dependencies for any dependency whose version contains SNAPSHOT.
  • application for application classes and resources.

The default order is dependencies, spring-boot-loader, snapshot-dependencies, and application. This is important as it determines how likely previous layers are to be cached when part of the application changes. If, for example, we put the application as the first layer, we will not be having any effect at all saving release time because every time we introduce a change all layers will be re-built. The layers should be sorted by having the least likely to change added first, followed by layers that are more likely to change.

The Spring Boot Gradle Plugin documentation contains a section explicitly referring to this point that can be located here. And, it should be enough to start experimenting with the feature, but as you all know, I am a hands-on person, then, let’s make a quick implementation.

The first thing you are going to need is a simple Spring Boot project, in my case is going to be based on Java 18 and Gradle. To easily get one ready to go, we can use the Spring Initializr.

To be able to build our first application using this feature, only two simple changes are required.

Modify the Gradle configuration to specify we want a layered build:

bootJar {
  layered()
}

The second modification is going to be in the Dockerfile:

FROM openjdk:18-alpine as builder
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM openjdk:18-alpine
COPY --from=builder dependencies/ ./
COPY --from=builder spring-boot-loader/ ./
COPY --from=builder snapshot-dependencies/ ./
COPY --from=builder application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

With only this, we will see already good improvements in our release times, but any seasoned developer will be pointing out soon that not all dependencies are equal. There is usually a huge difference in the release cycles between internal and external dependencies. The former tend to change quite often, especially if they are just modules of the same project, while the latter tend to be more stable and versions are upgraded more carefully. In the previous scenario, if the dependencies layer is the first one and it contains internal dependencies the more likely scenario is that all layers are going to be re-built all the time, and that is bad.

For that reason, the layered jars feature allows the specification of custom layers. To tackle the problem we have just mentioned, we can add just a few modifications to our files.

We can extend the recently added Gradle configuration with the specification of layers we want to use:

bootJar {
    layered {
        application {
            intoLayer("spring-boot-loader") {
                include("org/springframework/boot/loader/**")
            }
            intoLayer("application")
        }
        dependencies {
            intoLayer("snapshot-dependencies") {
                include("*:*:*SNAPSHOT")
            }
            intoLayer("internal-dependencies") {
                include("dev.binarycoders:*:*")
            }
            intoLayer("dependencies")
        }
        layerOrder = ["dependencies",
                      "spring-boot-loader",
                      "internal-dependencies",
                      "snapshot-dependencies",
                      "application"
        ]
    }
}

As we can see, we have added a few more layers and ordered them at our convenience.

  • internal-dependencies will build all our internal modules if we are using proper artefact names.
  • snapshot-dependencies will build any SNAPSHOT dependencies we are using.
  • spring-boot-loader will build the loader context.

The second change is to update our Dockerfile to reflect these changes:

FROM openjdk:18-alpine as builder
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM openjdk:18-alpine
COPY --from=builder dependencies/ ./
COPY --from=builder spring-boot-loader/ ./
COPY --from=builder internal-dependencies/ ./
COPY --from=builder snapshot-dependencies/ ./
COPY --from=builder application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

Basically, it has the same content but using the new layers.

Spring Boot layered deployment

Docker: Parent context

Sometimes when building Docker images, if we have custom files or resources we want to use and multiple images are going to use them, it is interesting to have then just once in a shared folder and to have all Dockerfile commands pointing to that shared folder.

The problem with this is that, when we are running a build, docker use as a context the current directory where the docker build command runs and, this, initially, can cause some fails.

Let’s see an example of this. Let’s imagine we have a few projects with a structure like this:

projectA -> DockerfileA
         -> fileA
         -> resources -> script.sh
         -> resources -> certs -> cert
projectB -> DockerfileA
         -> fileB
         -> resources -> script.sh
         -> resources -> certs -> cert
projectC -> DockerfileA
         -> fileC
         -> resources -> script.sh
         -> resources -> certs -> cert

In the above folder structure, we have three different projects and we can see we have some duplicated resources making more difficult to maintain and been error-prone. For this reason, we decide to restructure the folders and shared resources in something like:

projects -> projectA -> DockerfileA
         -> projectA -> fileA
         -> projectB -> DockerfileB
         -> projectB -> fileB
         -> projectC -> DockerfileC
         -> projectC -> fileC
         -> resources -> script.sh
         -> resources -> certs -> cert

As we can see, the above structure seems easier to maintain and less error-prone. The only consideration we need to have now is the docker build context.

Let’s say our Dockerfile files, among others, has the ADD or COPY commands inside. In this case, something like:

...
COPY ./resources/script.sh /opt/
COPY ./resources/certs/cert /opt/cert
...

Building docker images under the first folder structure is something like:

(pwd -> ~/projectA)
$ docker build -t projectA .

(pwd -> ~/projectB)
$ docker build -t projectB .

(pwd -> ~/projectC)
$ docker build -t projectC .

But, if we try to do it in the same way using the second folder structure we are going to be prompted with an error:

COPY failed: Forbidden path outside the build context.

This is because the context when executing the docker command does not have access to the folders allocated in the parent folder.

To avoid this problem, the only thing we need to do is to execute the build command from the parent folder and to add an extra flag (-f) to our command to point to the Dockerfile file we want to use when building the image. Something like:

(pwd -> projects)
$ docker build -t projectA -f projectA/DockerfileA .
$ docker build -t projectB -f projectB/DockerfileB .
$ docker build -t projectC -f projectC/DockerfileC .

This should solve the problem as now, the context of the docker build command is the parent folder.

Docker: Parent context

Docker Java Client

No questions about containers been one of the latest big things. They are everywhere, everyone use them or want to use them and the truth is they are very useful and they have change the way we develop.

We have docker for example that provides us with docker and docker-compose command line tools to be able to run one or multiple containers with just a few lines or a configuration file. But, we, developers, tend to be lazy and we like to automize things. For example, what about when we are running our application in our favorite IDE the application starts the necessary containers for us? It will be nice.

The point of this article was to play a little bit the a docker java client library, the case exposed above is just an example, probably you readers can think about better cases but, for now, this is enough for my learning purposes.

Searching I have found two different docker java client libraries:

I have not analyze them or compare them, I have just found the one by GitHub before and it looks mature and usable enough. For this reason, it is the one I am going to use for the article.

Let’s start.

First, we need to add the dependency to our pom.xml file:

<dependency>
    <groupId>com.github.docker-java</groupId>
    <artifactId>docker-java</artifactId>
    <version>3.1.1</version>
</dependency>

The main class we are going to use to execute the different instructions is the DockerClient class. This is the class that establishes the communication between our application and the docker engine/daemon in our machine. The library offers us a very intuitive builder to generate the object:

DockerClient dockerClient = DockerClientBuilder.getInstance().build();

There are some options that we can configure but for a simple example is not necessary. I am just going to say there is a class called DefaultDockerClientConfig where a lot of different properties can be set. After that, we just need to call the getInstance method with our configuration object.

Image management

Listing images

List<Image> images = dockerClient.listImagesCmd().exec();

Pulling images

dockerClient.pullImageCmd("postgres")
                .withTag("11.2")
                .exec(new PullImageResultCallback())
                .awaitCompletion(30, TimeUnit.SECONDS);

Container management

Listing containers

// List running containers
dockerClient.listContainersCmd().exec();

// List existing containers
dockerClient.listContainersCmd().withShowAll(true).exec();

Creating containers

CreateContainerResponse container = dockerClient.createContainerCmd("postgres:11.2")
                .withName("postgres-11.2-db")
                .withExposedPorts(new ExposedPort(5432, InternetProtocol.TCP))
                .exec();

Starting containers

dockerClient.startContainerCmd(container.getId()).exec();

Other

There are multiple operations we can do with containers, the above is just a short list of examples but, we can extended with:

  • Image management
    • Listing images: listImagesCmd()
    • Building images: buildImageCmd()
    • Inspecting images: inspectImageCmd("id")
    • Tagging images: tagImageCmd("id", "repository", "tag")
    • Pushing images: pushImageCmd("repository")
    • Pulling images: pullImageCmd("repository")
    • Removing images: removeImageCmd("id")
    • Search in registry: searchImagesCmd("text")
  • Container management
    • Listing containers: listContainersCmd()
    • Creating containers: createContainerCmd("repository:tag")
    • Starting containers: startContainerCmd("id")
    • Stopping containers: stopContainerCmd("id")
    • Killing containers: killContainerCmd("id")
    • Inspecting containers: inspectContainerCmd("id")
    • Creating a snapshot: commitCmd("id")
  • Volume management
    • Listing volumes: listVolumesCmd()
    • Inspecting volumes: inspectVolumeCmd("id")
    • Creating volumes: createVolumeCmd()
    • Removing volumes: removeVolumeCmd("id")
  • Network management
    • Listing networks: listNetworksCmd()
    • Creating networks: createNetworkCmd()
    • Inspecting networks: inspectNetworkCmd().withNetworkId("id")
    • Removing networks: removeNetworkCmd("id")

And, that is all. There is a project suing some of the operations listed here. It is call country, it is one of my learning projects, and you can find it here.

Concretely, you can find the code using the docker java client library here, and the code using this library here, specifically in the class PopulationDevelopmentConfig.

I hope you find it useful.

Docker Java Client

Docker for Java developers

Today, it has been one of this days to learn something new. If you have seen any of my blogs, you should know that I like to write about what I am learning and I like to write it myself, I am not a fan of just paste resources or other people’s materials or tutorials but, sometimes, I need to make exceptions.

Today, I was learning Docker, you know, the container platform that allows us to deploy applications in containers in a easy way. Unless you have been living for a long time under a rock probably you what I am talking about.

Looking for resources I have found a video of one of the presentation in the Devnexus conferences in 2015 that it has help me a lot. Considering it is a great resource, I am gonna link the video here, instead of writing myself an article, because I consider that it is very well explained and the tricks and solutions he shows are very interesting.

The speaker is Burr Sutter, a former employee (at the time the article is written) of Red Hat. He has a blog, a github account and a twitter account you can check. The video is quite interesting and not boring at all, it is more than one hour but it feels much shorter than that.

In addition, I strongly recommend you to go to his github account (listed before) and take a look to the tutorials he is using during the video:

  • Docker tutorial: Simple deploy of a Java EE application.
  • Docker MySQL tutorial: Same application than the previous one but with two different containers, one with the application server and the application and another one with the MySQL database.

Just a tiny addition to the excellent job he has done. Right now, we can download from the Docker page the native version of Docker for MAC and for Windows. They are still in beta version but I have not had any problem with that and everything looks a bit simpler.

Good luck with your learning and enjoy it.

See you.

Docker for Java developers