About
Side Projects
Blog
2022-04-29

Building Java Software with Docker

Introduction

Docker Images are a great way to template an environment for running software. When using Docker, one can be certain across different host operating systems and platforms exactly how the software will be run. This assurance allows for more reliable and safer software runtimes compared to configuring computers or virtual machines manually.

Software builds have similar requirements; environmental consistency and being able to run the build in different settings – shifted-left to a desktop computer through to a Kubernetes environment – is very beneficial for a software engineering team. This article covers using “multi stage” Docker builds for building Java applications. The same techniques are able to be employed for other languages as well.

Background on Docker

A running environment in Docker is known as a Container and a template for a Docker container is known as an Image which is essentially a “disk image”. To create a Docker Image, one describes all the things one wants to add into the Image using a specification called a Dockerfile. “Lines” in the Dockerfile add a Layer to the Image. Each Layer contributes file objects to the disk system. When an Image is run in Docker as a Container, a final layer is added on top of the Image that belongs to the Container and contains any files that are manipulated by the Container. This way the underlying Image can be used multiple times by multiple Containers such that the underlying Image does not change; it is immutable.

Docker Image Layers

Multi-Stage Dockerfile for Building Software

The way that Docker Images are composed from Layers in this way can be utilised as the basis for a build mechanism. Here is an example multi-stage Dockerfile illustrating how a build system could be put together in this way;

FROM maven:3.8.5-openjdk-17-slim as build
WORKDIR /src
COPY ./pom.xml .
RUN mvn dependency:go-offline
COPY . .
RUN mvn clean install

FROM amazoncorretto:17.0.2 as app
WORKDIR /app
COPY --from=build /src/target/*-exec.jar app.jar
ENTRYPOINT exec java -jar app.jar

Because of the two FROM statements, two Images will be created here. File objects from the first Image are copied into the latter Image using the --from argument to the COPY command.

Docker Image From

The first Image is used just for the building of the software and the second Image just for the eventual running of the software. The build product from the build Image is copied into the deployment Image. The intent of the Dockerfile above can be described in natural language;

By splitting the build and deploy Images, the deploy Image does not contain the source code, intermediate build files, build-time dependencies, files generated during testing and build tools. By not including this material in the build Image, the build image is smaller and there are less security risks presented from having the sundry and unnecessary files present.

Building the Docker Images

The following command would be used to build the two Images.

docker build .

Assembling the Docker Image on a local development environment the first time is slow because the statement mvn dependency:go-offline will download all the dependencies from the internet required for the project.

Docker will retain the Layers that it used to build an Image and so, assuming that only source other than the pom.xml files change, Docker is able to avoid the download of the dependencies on later Image builds saving time.

Docker Rebuild Layers

This use of cached Layers does not benefit ephemeral pipeline builds; this aspect is outside the scope of this article.

Summary

Docker multi-stage approaches are able to provide a way to build Java software projects locally. This build system has the advantage of being reliably repeatable and portable.