Create your FREE Codefresh account and start making pipelines fast. Create Account

Three Ways to Create Docker Images for Java

6 min read

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. If you want to follow along feel free to clone my repository at https://github.com/annabaker/docker-with-java-demos.

Prerequisites

First way: 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:

Paste the following and save:

  • 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:

…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.

To run the container from the image we just created:

-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:

Spring inside docker
Spring inside docker

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

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 and JDK 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

Second way: Normal Docker Build

In a “normal” Docker build, Docker will control the build process.
Modify the previous Dockerfile to contain the following:

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

And run the container:

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 or the JDK 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)

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:

Build the image:

And then run the container:

Pros to this approach:

  • Results in a light-weight Docker image
  • Does not require the build tool or JDK 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.

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:

You should see something like the following:

Docker image size comparison
Docker image size comparison

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 at https://github.com/annabaker/docker-with-java-demos

In a future post I will show you how to deal with caching issues specifically for Java builds.

Anna Baker

Anna Baker

Anna Baker is a Software Engineer/Technical Writer. She previously worked at Red Hat and is passionate about the open source community. In her free time, she enjoys drawing and cooking dishes from all over the world.

6 responses to “Three Ways to Create Docker Images for Java

Leave a Reply

* All fields are required. Your email address will not be published.