Three Ways to Create Docker Images for Java

Screen-Shot-2018-04-03-at-4.40.29-PM

Introduction

Long before Dockerfiles, Java developers worked with single deployment units (WARs, JARs, EARs, etc.).  As you likely know by now, it is best practice to work in micro-services, deploying a small number of deployment units per JVM.  Instead of one giant, monolithic application, you build your application such that each service can run on its own.

This is where Docker comes in!  If you wish to upgrade a service, rather than redeploying your jar/war/ear to a new instance of an application server, you can just build a new Docker image with the upgraded deployment unit.

In this post, I will review 3 different ways to create Docker images for Java applications.

Prerequisites

  • Docker is installed
  • Maven is installed (for example #1)
  • You have a simple Spring Boot application (I used the Spring Initializr project generator with a Spring Web dependency)

1.  Package-only Build

In a package-only build, we will let Maven (or your build tool of choice) control the build process.

Unzip the Spring Initializr project you generated as part of the prerequisites.  In the parent folder of your Spring Boot application, create a Dockerfile.  In a terminal, run:

$ unzip demo.zip
$ cd demo
$ nano Dockerfile

Paste the following and save:

# we will use openjdk 8 with alpine as it is a very small linux distro
FROM openjdk:8-jre-alpine3.9

# copy the packaged jar file into our docker image
COPY target/demo-0.0.1-SNAPSHOT.jar /demo.jar

# set the startup command to execute the jar
CMD ["java", "-jar", "/demo.jar"]
  • The FROM layer denotes which parent image to use for our child image
  • The COPY layer will copy the local jar previously built by Maven into our image
  • The CMD layer tells Docker the command to run inside the container once the previous steps have been executed

Now, let’s package our application into a .jar using Maven:

$ mvn clean package

…and then build the Docker image.  The following command tells Docker to fetch the Dockerfile in the current directory (the period at the end of the command).  We build using the username/image name convention, although this is not mandatory.  The -t flag denotes a Docker tag, which in this case is 1.0-SNAPSHOT.  If you don’t provide a tag, Docker will default to the tag :latest.

$ docker build -t anna/docker-package-only-build-demo:1.0-SNAPSHOT .

To run the container from the image we just created:

$ docker run -d -p 8080:8080 anna/docker-package-only-build-demo:1.0-SNAPSHOT

-d will run the container in the background (detached mode), and -p will map our local port 8080 to the container’s port of 8080.

Navigate to localhost:8080, and you should see the following:

Screenshot from 2020-02-25 17-16-48

Once you are satisfied with your testing, stop the container.

$ docker stop <container_id>

Pros to this approach:

  • Results in a light-weight Docker image
  • Does not require Maven to be included in the Docker image
  • Does not require any of our application’s dependencies to be packaged into the image
  • You can still utilize your local Maven cache upon application layer changes, as opposed to methods 2 and 3 which we will discuss later

Cons to this approach:

  • Requires Maven to be installed on the host machine
  • The Docker build will fail if the Maven build fails/is not executed beforehand — this becomes a problem when you want to integrate with services that automatically “just build” using the present Dockerfile

2.  Normal Docker Build

In a “normal” Docker build, Docker will control the build process.

Modify the previous Dockerfile to contain the following:

# select parent image
FROM maven:3.6.3-jdk-8

# copy the source tree and the pom.xml to our new container
COPY ./ ./

# package our application code
RUN mvn clean package

# set the startup command to execute the jar
CMD ["java", "-jar", "target/demo-0.0.1-SNAPSHOT.jar"]

Now, let’s build a new image as we did in Step 1:

$ docker build -t anna/docker-normal-build-demo:1.0-SNAPSHOT .

And run the container:

$ docker run -d -p 8080:8080 anna/docker-normal-build-demo:1.0-SNAPSHOT

Again, to test your container, navigate to localhost:8080.  Stop the container once you are finished testing.

Pros to this approach:

  • Docker controls the build process, therefore this method does not require the build tool to be installed on the host machine beforehand
  • Integrates well with services that automatically “just build” using the present Dockerfile

Cons to this approach:

  • Results in the largest Docker image of our 3 methods
  • This build method not only packaged our app, but all of its dependencies and the build tool itself, which is not necessary to run the executable
  • If the application layer is rebuilt, the mvn package command will force all Maven dependencies to be pulled from the remote repository all over again (you lose the local Maven cache)

3.  Multi-stage Build (The ideal way)

With multi-stage Docker builds, we use multiple FROM statements for each build stage.  Every FROM statement creates a new base layer, and discards everything we don’t need from the previous FROM stage.

Modify your Dockerfile to contain the following:

# the first stage of our build will use a maven 3.6.1 parent image
FROM maven:3.6.1-jdk-8-alpine AS MAVEN_BUILD

# copy the pom and src code to the container
COPY ./ ./

# package our application code
RUN mvn clean package

# the second stage of our build will use open jdk 8 on alpine 3.9
FROM openjdk:8-jre-alpine3.9

# copy only the artifacts we need from the first stage and discard the rest
COPY --from=MAVEN_BUILD /docker-multi-stage-build-demo/target/demo-0.0.1-SNAPSHOT.jar /demo.jar

# set the startup command to execute the jar
CMD ["java", "-jar", "/demo.jar"]

Build the image:

$ docker build -t anna/docker-multi-stage-build-demo:1.0-SNAPSHOT .

And then run the container:

$ docker run -d -p 8080:8080 anna/docker-multi-stage-build-demo:1.0-SNAPSHOT

Pros to this approach:

  • Results in a light-weight Docker image
  • Does not require the build tool to be installed on the host machine beforehand (Docker controls the build process)
  • Integrates well with services that automatically “just build” using the present Dockerfile
  • Only artifacts we need are copied from one stage to the next (i.e., our application’s dependencies are not packaged into the final image as in the previous method)
  • Create as many build stages as you need
  • Stop at any particular stage on an image build using the –target flag, i.e.
    docker build --target MAVEN_BUILD -t anna/docker-multi-stage-build-demo:1.0-SNAPSHOT .

Cons to this approach:

  • If the application layer is rebuilt, the mvn package command will force all Maven dependencies to be pulled from the remote repository all over again (you lose the local Maven cache)

Verification:  How big are the images?

In a terminal, run:

docker image ls

You should see something like the following:

Screenshot from 2020-02-25 15-52-59

As you can see, the multi-stage build resulted in our smallest image, whereas the normal build resulted in our largest image.  This should be expected since the normal build included our application code, all of its dependencies, and our build tooling, and our multi-stage build contained only what we needed.

Conclusion

Of the three Docker image build methods we covered, Multi-stage builds are the way to go.  You get the best of both worlds when packaging your application code — Docker controls the build, but you extract only the artifacts you need.  This becomes particularly important when storing containers on the cloud.

  • You spend less time building and transferring your containers on the cloud, as your image is much smaller
  • Cost — the smaller your image, the cheaper it will be to store
  • Smaller surface area, aka removing additional dependencies from our image makes it less prone to attacks

Thanks for following along, and I hope this helps!  You can find the source code for all three examples in my github, here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s