1. Introduction

We have seen where we already have many of the tools we need to be able to develop, test, and deploy a functional application. However, there will become a point where things will get complicated.

  • What if everything is not a Spring Boot application and requires a unique environment?

  • What if you end up with dozens of applications and many versions?

    • Will everyone on your team be able to understand how to instantiate them?

Lets take a user-level peek at the Docker container in order to create a more standardized look to all our applications.

1.1. Goals

You will learn:

  • the purpose of an application container

  • to identify some open standards in the Docker ecosystem

  • to build a Docker images using different techniques

  • to build a layered Docker image

1.2. Objectives

At the conclusion of this lecture and related exercises, you will be able to:

  1. build a basic Docker image with an executable JAR using a Dockerfile and docker commands

  2. build a basic Docker image with the Spring Boot Maven Plugin and buildpack

  3. build a layered Docker image with the Spring Boot Maven Plugin and buildpack

  4. build a layered Docker image using a Dockerfile and docker commands

  5. run a docker image hosting a Spring Boot application

2. Containers

A container is a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another. A Docker container image is a lightweight, standalone, executable package of software that includes everything needed to run an application: code, runtime, system tools, system libraries and settings.
— docker.com
"What is a Container" A standardized unit of software

2.1. Container Deployments

The following diagrams represent three common application deployment strategies: native, virtual machine, and container.

container applications
Figure 1. Native Deployment
container vms
Figure 2. VM Deployment
container containers
Figure 3. Container Deployment
  • native - has the performance advantage of running on bare metal but the disadvantage of having full deployment details exposed and the vulnerability of directly sharing the same host operating system with other processes.

  • virtual machine - (e.g., VMWare, VirtualBox) has the advantage of isolation from other processes and potential encapsulation of installation details but the disadvantage of a separate and distinct guest operating systems running on the same host with limited sharing of resources.

  • container - has the advantage of isolation from other processes, encapsulation of installation details, and runs in a lightweight container runtime that efficiently shares the resources of the host OS with each container.

3. Docker Ecosystem

Docker is an ecosystem of tooling that covers a lot of topics. Two of which are the container image and runtime. The specifications of both of these have been initiated by Docker — the company — and transitioned to the Open Container Initiative (OCI) — a standards body — that maintains the definition of the image and runtime specs and certifications.

This has allowed independent toolsets (for building Docker images) and runtimes (for running Docker images under different runtime and security conditions). For example, the following is a sample of the alternative builders and runtimes available.

3.1. Container Builders

Docker — the company — offers a Docker image builder. However, the builder requires a daemon with a root-level installation. Some of the following simply implement a builder tool:

I use kaniko on a daily basis to build images within a CI/CD build pipeline. Since the jobs within the pipeline all run within Docker images, it helps avoid having to setup Docker within Docker and running the images in privileged mode.

3.2. Container Runtimes

Docker — the company — offers a container runtime. However, this container runtime has a complex lifecycle that includes daemons and extra processes. Some of the following simply run an image.

4. Docker Images

A Docker image is a tar file of layered, intermediate levels of the application. A layer within a Docker image contains a tar file of the assigned artifacts for that layer. If two or more Docker files share the same base layer — there is no need to repeat the base layer in that repository. If we change the upper levels of a Docker file, there is no need to rebuild the lower levels. These aspects will be demonstrated within this lecture and optimized in the tooling available to use within Spring Boot.

5. Basic Docker Image

We can build a basic Docker image from a normal executable JAR created from the Spring Boot Maven Plugin. To prove that — we will return to the hello-docker-example used in the previous Heroku deployment lecture.

Example Requires Docker Installed
Implementing the first example will require docker — the product — to be installed. Please see the development environment Docker setup for references.

The following shows us starting with a typical example web application that listens to port 8080 when built and launched. The build happens to automatically invoke the spring-boot:repackage goal. However, if that is not the case, just run mvn spring-boot:repackage to build the Spring Boot executable JAR.

Building and Running Basic Docker Image
$ mvn clean package (1)
...
target/
|-- [ 31M]  docker-hello-example-6.0.1-SNAPSHOT-SNAPSHOT-bootexec.jar
|-- [9.7K]  docker-hello-example-6.0.1-SNAPSHOT.jar

$ java -jar target/docker-hello-example-6.0.1-SNAPSHOT-bootexec.jar (2)
...
Tomcat started on port(s): 8080 (http) with context path ''
Started DockerHelloExampleApp in 3.058 seconds (JVM running for 3.691)
1 building the executable Spring Boot JAR
2 running the application

5.1. Basic Dockerfile

We can build a basic Docker image manually by adding a Dockerfile and issuing a Docker command to build it.

The basic Dockerfile below extends a base OpenJDK 17 image from the global Docker repository, adds the executable JAR, and registers the default commands to use when running the image. It happens to have the name Dockerfile.execjar, which will be referenced by a later command.

Example Basic Dockerfile (named Dockerfile.execjar)
FROM openjdk:17.0.2 (1)
COPY target/*-bootexec.jar application.jar (2)
ENTRYPOINT ["java", "-jar", "application.jar"] (3)
1 building off a base openjdk 14 image
2 copying executable JAR into the image
3 establishing default command to run the executable JAR

5.2. Basic Docker Image Build Output

The Docker build command processes the Dockerfile and produces an image. We supply the Dockerfile, the directory (.) of the source files referenced by the Dockerfile, and an image name and tag.

Example docker build Command Output
$ docker build . -f Dockerfile.execjar -t docker-hello-example:execjar (1) (2) (3) (4)
...
=> [1/2] FROM docker.io/library/openjdk:17.0 ...         5.3s
=> [2/2] COPY target/*-bootexec.jar application.jar      0.8s
...
Step 1/3 : FROM adoptopenjdk:14-jre-hotspot
Step 2/3 : COPY target/*.jar application.jar
Step 3/3 : ENTRYPOINT ["java", "-jar", "application.jar"]
Successfully built eda93db54671
Successfully tagged docker-hello-example:execjar
1 build - command to build Docker image
2 . - current directory is default source
3 -f - path to Dockerfile, if not Dockerfile in current directory
4 name:tag - name and tag of image to create
Dockerfile is default name for Dockerfile
Default Docker file name is Dockerfile. This example will use multiple Dockerfiles, so the explicit -f naming has been used.

5.3. Local Docker Registry

Once the build is complete, the image is available in our local repository with the name and tag we assigned.

Example Local Repository
$ docker images | egrep 'docker-hello-example|REPO'
REPOSITORY             TAG      IMAGE ID       CREATED          SIZE
docker-hello-example   execjar  eda93db54671   12 minutes ago   504MB
  • REPOSITORY - names the primary name of the Docker image

  • TAG - primarily used to identify versions and variants of repository name. latest is the default tag

  • IMAGE ID - is a hex string value that identifies the image. The repository:tag label just happens to point to that version right now, but will advance in a future change/build.

  • SIZE - is total size if exported. Since Docker images are layered, multiple images sharing the same base image will supply much less overhead than reported here while staged in a repository

5.4. Running Docker Image

We can run the image with the docker run command. The following example shows running the docker-hello-image with tag execjar, exposing port 8080 within the image as port 9090 on localhost (-p 9090:8080), running in interactive mode (-it; optional here, but important when using as interactive shell), and removing the runtime image when complete (--rm).

Example Docker Run Command
$ docker run --rm -it -p 9090:8080 docker-hello-example:execjar (1) (2) (3) (4)
  .   ____          _            __ _ _ (5)
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (2.7.0)
...
Tomcat started on port(s): 8080 (http) with context path ''
Started DockerHelloExampleApp in 4.049 seconds (JVM running for 4.784)
1 run - run a command in a new Docker image
2 --rm - remove the image instance when complete
3 -it allocate a pseudo-TTY (-t) for an interactive (`-i) shell
4 -p - map external port 9090 to 8080 of the internal process
5 Spring Boot App launched with no arguments

5.5. Docker Run Command with Arguments

Arguments can also be passed into the image. The example below passes in a standard Spring Boot property to turn off printing of the startup banner.

Example Docker Run Command with Arguments
$ docker run --rm -it -p 9090:8080 docker-hello-example:execjar --spring.main.banner-mode=off
... (1)
Tomcat started on port(s): 8080 (http) with context path ''
Started DockerHelloExampleApp in 4.049 seconds (JVM running for 4.784)
1 spring.main.banner-mode property passed to Spring Boot App and disabled banner printing

5.6. Running Docker Image

We can verify the process is running using the Docker ps command.

Example docker ps Command
$ docker ps
CONTAINER ID  IMAGE                         COMMAND                 CREATED         STATUS         PORTS                    NAMES
8078f6369a59  docker-hello-example:execjar  "java -jar applicati…"  4 minutes ago   Up 4 minutes   0.0.0.0:9090->8080/tcp   practical_agnesi
  • CONTAINER ID - hex string we can use to refer to this running (or later terminated) instance

  • IMAGE - REPO:TAG executed

  • COMMAND - command executed upon entry

  • CREATED - when started

  • STATUS - run status. Use docker ps -a to locate all images and not just running images

  • PORTS - lists ports exposed within image and what they are mapped to externally on the host

  • NAMES - textual name alias for instance. Can be used interchangeably with containerId. Can be explicitly set with --name foo option prior to the image parameter, but must be unique

5.7. Using the Docker Image

We can call our Spring Boot process within the image using the mapped 9090 port.

$ curl http://localhost:9090/api/hello?name=jim
hello, jim

5.8. Docker Image is Layered

The Docker image is a TAR file that is made up of layers

Example Docker Image Tarfile Contents
$ docker save docker-hello-example:execjar > image.tar
Mac:image$ tar tf image.tar
27dcc15ccaaac941791ba5826356a254e70c85d4c9c8954e9c4eb2873506a4c8/
27dcc15ccaaac941791ba5826356a254e70c85d4c9c8954e9c4eb2873506a4c8/VERSION
27dcc15ccaaac941791ba5826356a254e70c85d4c9c8954e9c4eb2873506a4c8/json
27dcc15ccaaac941791ba5826356a254e70c85d4c9c8954e9c4eb2873506a4c8/layer.tar
304740117a5a0c15c8ea43b7291479207b357b9fc08cc47a5e4a357f5e9a1768/
304740117a5a0c15c8ea43b7291479207b357b9fc08cc47a5e4a357f5e9a1768/VERSION
304740117a5a0c15c8ea43b7291479207b357b9fc08cc47a5e4a357f5e9a1768/json
304740117a5a0c15c8ea43b7291479207b357b9fc08cc47a5e4a357f5e9a1768/layer.tar
...
a3651512f2a9241ae11ad8498df67b4f943ea4943f4fae8f88bcb0b81168803d/
a3651512f2a9241ae11ad8498df67b4f943ea4943f4fae8f88bcb0b81168803d/VERSION
a3651512f2a9241ae11ad8498df67b4f943ea4943f4fae8f88bcb0b81168803d/json
a3651512f2a9241ae11ad8498df67b4f943ea4943f4fae8f88bcb0b81168803d/layer.tar
...
manifest.json
repositories

This specific example has seven (7) layers.

Example Layer Count
$ tar tf image.tar | grep layer.tar | wc -l
       7

5.9. Application Layer

If we untar the Docker image and poke around, we can locate the layer that contains our executable JAR file. All 25M of it in one place.

Example Application Layer
$ tar tf ./a3651512f2a9241ae11ad8498df67b4f943ea4943f4fae8f88bcb0b81168803d/layer.tar
application.jar (1)

ls -lh ./a3651512f2a9241ae11ad8498df67b4f943ea4943f4fae8f88bcb0b81168803d/layer.tar
25M ./a3651512f2a9241ae11ad8498df67b4f943ea4943f4fae8f88bcb0b81168803d/layer.tar
1 one of the layers contains our application layer and is made up of a single Spring Boot executable JAR

There are a few things to note about what we uncovered in this section

  1. the Docker image is not a closed, binary representation. It is an openly accessible layer of files as defined by the OCI Image Format Specification.

  2. our application is currently implemented as a single 25MB layer with a single Spring Boot executable JAR. Our code was likely only a few KBytes of that 25MB.

Hold onto both of those points when covering the next topic.

5.10. Spring Boot Plugin

Starting with Spring Boot 2.3 and its enhanced support for cloud technologies, the Spring Boot Maven Plugin now provides support for building a Docker image using buildpack — not Docker and no Dockerfile.

$ mvn spring-boot:help
...
spring-boot:build-image
  Package an application into a OCI image using a buildpack.

Buildpack is an approach to building Docker images based on strict layering concepts that Docker has always prescribed. The main difference with buildpack is that the layers are more autonomous — backed by a segment of industry — allowing for higher level application layers to be quickly rebased on top of patched operating system layers without fully rebuilding the image.

Joe Kutner from Heroku stated at a Spring One Platform conference that they were able to patch 10M applications overnight when a serious bug was corrected in a base layer. This was due to being able to rebase the application specific layers with a new base image using buildpack technology and without having to rebuild the images. [1]

5.11. Building Docker Image using Buildpack

If we look at the portions of the generated output, we will see

  • 15 candidate buildpacks being downloaded

  • one of the 5 used buildpacks is specific to spring-boot

  • various layers are generated and reused to build the image

  • our application still ends up in a single layer

  • the image is generated, by default using the Maven artifactId as the image name and version number as the tag

Example Maven Building using Buildpack
$ mvn clean package spring-boot:build-image -DskipTests
...
[INFO] --- spring-boot-maven-plugin:2.7.0:build-image (default-cli) @ docker-hello-example ---
[INFO] Building image 'docker.io/library/docker-hello-example:6.0.1-SNAPSHOT'
[INFO]
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 6%
...
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 100%
[INFO]  > Pulled builder image 'gcr.io/paketo-buildpacks/builder@sha256:6d625fe00a2b5c4841eccb6863ab3d8b6f83c3138875f48ba69502abc593a62e'
[INFO]  > Pulling run image 'gcr.io/paketo-buildpacks/run:base-cnb' 100%
[INFO]  > Pulled run image 'gcr.io/paketo-buildpacks/run@sha256:087a6a98ec8846e2b8d75ae1d563b0a2e0306dd04055c63e04dc6172f6ff6b9d'
[INFO]  > Executing lifecycle version v0.8.1
[INFO]  > Using build cache volume 'pack-cache-2432a78c0232.build'
[INFO]
[INFO]  > Running creator
[INFO]     [creator]     ===> DETECTING
[INFO]     [creator]     5 of 16 buildpacks participating
...
[INFO]     [creator]     paketo-buildpacks/spring-boot       2.4.1
...
[INFO]     [creator]     ===> EXPORTING
[INFO]     [creator]     Reusing layer 'launcher'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:class-counter'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
...
[INFO]     [creator]     Adding 1/1 app layer(s)
[INFO]     [creator]     Adding layer 'config'
[INFO]     [creator]     *** Images (10a764b20812):
[INFO]     [creator]           docker.io/library/docker-hello-example:6.0.1-SNAPSHOT
[INFO]
[INFO] Successfully built image 'docker.io/library/docker-hello-example:6.0.1-SNAPSHOT'

5.12. Buildpack Image in Local Docker Repository

The newly built image is now installed into the local Docker registry. It is using the Maven GAV artifactId for the repository and version for the tag.

Docker Repository with both Images
$ docker images | egrep 'docker-hello-example|IMAGE'
REPOSITORY                 TAG              IMAGE ID       CREATED          SIZE
docker-hello-example       execjar          eda93db54671   40 minutes ago   315MB (1)
docker-hello-example       6.0.1-SNAPSHOT   10a764b20812   41 years ago     279MB (1)
1 NOTE: sizes were from a later build using newer versions of Spring Boot
One odd thing is the timestamp used (41 years ago) for the created date with the build pack image. Since it is referring to the year 1970 (new java.util.Date(0) UTC), we can likely assume there was a 0 value in a timestamp field somewhere.

5.13. Buildpack Image Execution

Notice that when we run the newly built image that was built with buildpack, we get a little different behavior at the beginning where some base level memory tuning is taking place.

Example Buildpack Image Execution
$ docker run --rm -it -p 9090:8080 docker-hello-example:6.0.1-SNAPSHOT
Container memory limit unset. Configuring JVM for 1G container.
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -XX:MaxMetaspaceSize=87032K -XX:ReservedCodeCacheSize=240M -Xss1M -Xmx449543K (Head Room: 0%, Loaded Class Count: 12952, Thread Count: 250, Total Memory: 1.0G)
Adding 127 container CA certificates to JVM truststore
Spring Cloud Bindings Boot Auto-Configuration Enabled
...
Tomcat started on port(s): 8080 (http) with context path ''
Started DockerHelloExampleApp in 3.589 seconds (JVM running for 4.3)

The following shows we are able to call the new running image.

Example Buildpack Image Call
$ curl http://localhost:9090/api/hello?name=jim
hello, jim

5.14. Inspecting Buildpack Image

If we save off the newly built image and briefly inspect, we will see that is contains the same TAR-based layering scheme but will 21 versus 7 layers in this specific example.

Buildpack Layer Count
$ docker save docker-hello-example:6.0.1-SNAPSHOT > image.tar
$ tar tf image.tar | grep layer.tar | wc -l
      21

If we untar the mage and poke around, we can eventually locate our application and notice that it happens to be in exploded form versus executable JAR form. We can see our code and dependency libraries separately.

Buildpack Application Layer
$ tar tf 6e2b5eb3b4b11627cce2ca7c8aeb7de68a7a54b56b15ea4d43e4a14d2b1f0b9a/layer.tar
...
/workspace/BOOT-INF/classes/info/ejava/examples/svc/docker/hello/DockerHelloExampleApp.class
/workspace/BOOT-INF/classes/info/ejava/examples/svc/docker/hello/controllers/ExceptionAdvice.class
/workspace/BOOT-INF/classes/info/ejava/examples/svc/docker/hello/controllers/HelloController.class
...
/workspace/BOOT-INF/lib/classgraph-4.8.69.jar
/workspace/BOOT-INF/lib/commons-lang3-3.10.jar
/workspace/BOOT-INF/lib/ejava-dto-util-6.0.1-SNAPSHOT.jar
/workspace/BOOT-INF/lib/ejava-util-6.0.1-SNAPSHOT.jar
/workspace/BOOT-INF/lib/ejava-web-util-6.0.1-SNAPSHOT.jar

As a reminder, when we built the Docker image with a Docker file and vanilla docker commands — we ended up with an application layer with a single, Spring Boot executable JAR (with a few KBytes of our code and 24.9 MB of dependency artifacts).

Review: Earlier Generic Docker Image Application Layer
$ tar tf ./a3651512f2a9241ae11ad8498df67b4f943ea4943f4fae8f88bcb0b81168803d/layer.tar
application.jar

ls -lh ./a3651512f2a9241ae11ad8498df67b4f943ea4943f4fae8f88bcb0b81168803d/layer.tar
25M ./a3651512f2a9241ae11ad8498df67b4f943ea4943f4fae8f88bcb0b81168803d/layer.tar

6. Layers

Dockerfile layers are an important concept when it comes to efficiency of storage and distribution. Any images built on common base images or intermediate commands that produce the same result do not have to be replicated within a repository. For example, 100 images all extending from the same OpenJDK 17 image do not need to have the OpenJDK 17 portions repeated.

To make it easier to view and analyze the layers of the Dockerfile — we can use a simple inspection tool called dive. This shows us how the image is constructed, where we may have wasted space, and potentially how to optimize. Since these images are brand new and based off production base images — we will not see much wasted space at this time. However, it will help us better understand the Docker image and how cloud features added to Spring Boot can help us.

Dive Not Required
There is no need to install the dive tool to learn about layers and how Spring Boot provides support for layers. All necessary information to understand the topic is contained in the following material.
Running dive on Docker Image
$ dive [imageId or name:tag]

With the image displayed, I find it helpful to:

  • hit [CNTL]+L if "Show Layer Changes is not yet selected"

  • hit [TAB] to switch to "Current Layer Contents" pane on the right

  • hit [CNTL]+U,R,M, and B to turn off all display except "Added"

  • hit [TAB] to switch back to "Layers" pane on the left

In the "Layers" pane we can scroll up and down the layers to see which files where added because of which ordered command in the Dockerfile. If all the layers look the same, make sure you are only displaying the "Added" artifacts.

Dive within Docker

Or — of course — you could run dive within Docker to inspect a Docker image. This requires that you map the image’s Docker socket to the host machine’s Docker socket with the -v syntax. This is likely OS-specific.

docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive [imageId or name:tag]

6.1. Analyzing Basic Docker Image

In this first example, we are looking at the layers of the basic Dockerfile. Notice:

  • a majority of the size was the result of extending the OpenJDK image. That space represents content that a Dockerfile repository does not have to replicate.

  • the last layer contains the 26MB executable JAR. Because that technically contains our custom application. This is content a Dockerfile repository has to replicate.

Analyzing Basic Docker Image
$ dive docker-hello-example:execjar
container docker dive

6.2. Analyzing Basic Buildpack Image

If we look at the Docker image built with buildpack, through the Maven plugin, we will see the same 26MB exploded as separate files towards the end of the image. From a layering perspective — the exploded structure has not saved us anything.

Analyzing Buildpack Image
$ dive docker-hello-example:6.0.1-SNAPSHOT
container buildpack dive

However, now that we have it exploded — we will have the option to break it into further layers.

7. Adding Fine-grain Layering

Having all 26MB of our Spring Boot application in a single layer can be wasteful — especially if we push new images to a repository many times during development. We end up with 26MB.version1, 26MB.version2, etc. when each push is more than likely a few modifications of class files within the application and a complete change in library dependencies not as common.

7.1. Configure Layer-ready Executable JAR

The Spring Boot plugin and buildpack provide support for creating finer-grain layers from the executable JAR by enabling the layers plugin configuration property.

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <layers>
            <enabled>true</enabled>
        </layers>
    </configuration>
</plugin>

7.2. Building and Inspecting Layer-ready Executable JAR

If we rebuild the executable JAR with the layered option, an extra wrapper is added to the executable JAR file that can be activated with the -Djarmode=layeredtools option to the java -jar command. This option takes one of two arguments: list or extract.

Inspecting Layer-ready Executable JAR
$ mvn clean package spring-boot:repackage -Dlayered=true -DskipTests (1)

$ java -Djarmode=layertools -jar target/docker-hello-example-6.0.1-SNAPSHOT-bootexec.jar
Usage:
  java -Djarmode=layertools -jar docker-hello-example-6.0.1-SNAPSHOT-bootexec.jar

Available commands:
  list     List layers from the jar that can be extracted
  extract  Extracts layers from the jar for image creation
  help     Help about any command
1 -Dlayered=true activates layering within the Maven pom.xml

7.3. Default Executable JAR Layers

Spring Boot automatically configures four (4) layers by default: (released) dependencies, spring-boot-loader, snapshot-dependencies, and application. These layers are ordered from most stable (dependencies) to least stable (application). We have the ability to change the layers — but I won’t go into that here.

Default Executable JAR Layers
$ java -Djarmode=layertools -jar target/docker-hello-example-6.0.1-SNAPSHOT-bootexec.jar list
dependencies
spring-boot-loader
snapshot-dependencies
application

8. Layered Buildpack Image

With the layers configuration property enabled, the next build will result in a layered image posted to the local Docker repository.

$ mvn package spring-boot:build-image -Dlayered=true -DskipTests
...
Successfully built image 'docker.io/library/docker-hello-example:6.0.1-SNAPSHOT'

8.1. Dependency Layer

The dependency layer contains all the released dependencies. This happens to make up most of the 26MB we had for the executable JAR. This 26MB does not need to be replicated in the image repository if consistent with follow-on publications of our image.

container buildpack layer1
Figure 4. Dependency Layer

8.2. Snapshot Layer

The snapshot layer contains dependency artifacts that have not been released. This is an indication that the artifact is slightly more stable than our application code but not as stable as the released dependencies.

container buildpack layer3
Figure 5. Snapshot Dependency Layer

8.3. Application Layer

The application layer contains the code for the local module — which should be the most volatile. Notice that in this example, the application code is 12KB out of the total 26MB for the executable JAR. If we change our application code and redeploy the image somewhere — only this small portion of the code needs to change.

container buildpack layer4
Figure 6. Application Layer

8.4. Review: Single Layer Application

If you remember …​ before we added multiple layers, all the library stable JARs and semi-stable SNAPSHOT dependencies were in the same layers as our potentially changing application code. We now have them in separate layers.

Review: Single Layer Application

container buildpack single lookback]

9. Layered Docker Image

Since buildpack may not be for everyone, Spring Boot provides a means for standard Docker users to create layered images with a standard Dockerfile and standard docker commands. The following example is based on the Example Dockerfile on the Spring Boot features page.

9.1. Example Layered Dockerfile

The Dockerfile is written in two parts: builder and image construction. The first, builder half of the file copies in the executable JAR and extracts the layer directories into a temporary portion of the image.

The second, construction half builds the final image by extending off what could be an independent parent image and the products of the builder phase. Notice how the four (4) layers are copied in separately - forming distinct boundaries.

Example Layered Dockerfile
FROM openjdk:17.0.2 as builder (1)
WORKDIR application
ARG JAR_FILE=target/*-bootexec.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM openjdk:17.0.2 (2)
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"]
1 commands used to setup building the image
2 commands used to build the image
Example Layered Dockerfile Build
$ docker build . -f Dockerfile.layered -t docker-hello-example:layered
Sending build context to Docker daemon   26.1MB

9.2. Example Build

The following shows the output of building our example using the docker build command and the Dockerfile above. Notice:

  • that it copies in the executableJAR and extracts the layers into the temporary image.

  • how it is building separate, distinct layers by using separate COPY commands for each layer directory.

Example Docker Image Construction Phase
 => [stage-1 2/6] WORKDIR application
 => [builder 3/4] COPY target/*-bootexec.jar application.jar
 => [builder 4/4] RUN java -Djarmode=layertools -jar application.jar extract
 => [stage-1 3/6] COPY --from=builder application/dependencies/ ./
 => [stage-1 4/6] COPY --from=builder application/spring-boot-loader/ ./
 => [stage-1 5/6] COPY --from=builder application/snapshot-dependencies/ ./
 => [stage-1 6/6] COPY --from=builder application/application/ ./
 => => naming to docker.io/library/docker-hello-example:layered

9.3. Dependency Layer

The dependency layer — like with the buildpack version — contains 26MB of the released JARs. This makes up the bulk of what was in our executable JAR.

container docker layer1

9.4. Snapshot Layer

The snapshot layer contains dependencies that have not yet been released. These are believed to be more stable than our application code but less stable than the released dependencies.

container docker layer3

9.5. Application Layer

The application layer contains our custom application code. This layer is thought to be the most volatile and is in the top-most layer.

container docker layer4

10. Summary

In this module we learned:

  • Docker is a ecosystem of concepts, tools, and standards

  • Docker — the company — provides an implementation of those concepts, tools, and standards

  • Docker images can be created using different tools and technologies

    • the docker build command uses a Dockerfile

    • buildpack uses knowledgeable inspection of the codebase

  • Docker images have ordered layers — from common operating system to custom application

  • buildpack layers are rigorous enough that they can be rebased upon freshly patched images — making hundreds to millions of image patches feasible within a short amount of time

  • intelligent separation of code into layers and proper ordering can lead to storage and complexity savings

  • Spring Boot provides a means to separate the executable JAR into layers that match certain criteria


1. "Pack to the Future: Cloud-Native Buildpacks on k8s", Spring One Platform, Oct 2019