Java 17 Multi-Stage Dockerfile

From Build to Lightweight Execution Using Scratch Images

Pedro Arrieta
Globant
5 min readMay 31, 2024

--

Photo by Ian Taylor on Unsplash

Nowadays, using containers to run an application is one of the most common ways; building a proper Docker image is an essential step in the application’s development. One of the best practices is the utilization of Multi-stage in Dockerfile. This practice or technique allows for optimizing the size of the final image, deleting dependencies, libraries, files, etc., which will not be used by the application, giving us, as a result, a lightweight and secure image.

The Multi-stage Dockerfile

Instead of having a single image that includes all the tools and dependencies necessary to build an application, multi-stage builds allow you to create an initial image with all the build tools and dependencies needed to build the image. After the build is finished, only the necessary artifacts are copied to the final image. This makes the final image lighter and more efficient.

Below, we will explore the key features and use cases of multi-stage Dockerfiles, highlighting their ability to improve efficiency and security in application development and deployment in containerized environments. You can find more information here and here too.

Key features

This is a list of key features of using a multi-stage Dockerfile:

  • Stages division: A multi-stage Dockerfile is divided into multiple stages, each with a specific set of instructions. Every stage serves a particular purpose in the image construction process. This approach allows for a modular and organized development workflow, where different aspects of the image build, such as compilation, testing, and artifact generation, are encapsulated within dedicated stages.
  • Isolated Environment: Each stage has its own isolated environment, which means that the dependencies, tools, and libraries required for a specific task are included only in that stage. Furthermore, this practice promotes consistency and reproducibility, as the dependencies encapsulated within each isolated environment are precisely defined, contributing to a reliable and predictable build and deployment process.
  • Artifact Transfer: This practice allows the transfer of artifacts or files generated in earlier stages to later stages. Likewise, it promotes greater coherence and efficiency in the construction of images by taking advantage of previously generated elements, avoiding redundancies, and optimizing the use of resources in developing the application.

Use cases

Multi-stage Dockerfiles are usually used with the following use cases:

  • Dependency Reduction: This practice allows the removal of development dependencies, tools, and libraries that are not necessary for your last image. By minimizing unnecessary dependencies, a cleaner and more efficient environment is achieved, reducing the overall size of the resulting image. Additionally, this approach contributes to greater security by limiting exposure to potential vulnerabilities present in non-essential dependencies.
  • Image Size Optimization: It is ideal for creating smaller and lightweight images. The ability to discard unnecessary components at later stages leads to more efficient final images, making them easier to distribute and deploy. Additionally, reducing image size improves transfer and deployment speed, optimizing the overall performance of the application development and delivery process.
  • Useful for CI/CD Processes: For the CI/CD (Continuous Integration and Continuous Delivery/Deployment) process, multistage Dockerfiles are key in reducing build times and ensuring that images used are minimal and safe.

The Scratch Image

What is a scratch image? It is an empty image used as a starting point for building minimal images. This is the type of image used to create all official base images you can find in Docker Hub.

Why is using a scratch image for your projects a good idea? We will explore the features and use cases of the Scratch image in container building:

Key Features

Below are the key features of a scratch image:

  • Remarkable size reduction: As this image does not include unnecessary elements, scratch images are incredibly small compared to other container images. This feature makes it easier to distribute and deploy applications quickly.
  • Enhanced security: By limiting the number of libraries, dependencies, etc., present in the container, the potential attack surface is reduced. This helps to have a more secure environment and minimizes the chances of security vulnerabilities.

Use Cases

Let’s review when it is a good idea to use scratch images:

  • Use for microservices: Scratch images can be the perfect choice for specific microservices with a single purpose. This ensures that only the essential components required for the application are included.
  • Minimized image construction: Scratch-based images are designed for optimal performance and rapid deployment. This is especially valuable in production environments where efficiency and speed are critical.

Prerequisites

Below, you will find the prerequisites you must know/have to understand this article fully:

  • Docker installation: You will need to install the Docker engine to proceed with this solution. You can find a how-to here.
  • Docker basic knowledge: It is important to know the basics of Docker to know how the Dockerfile works. You will need to know the basics of how Docker works. You can find more information here and here.
  • Java application building and compiling: It is essential to understand and know which commands and steps are required to build and compile your application, regardless of the project, such as Maven or Gradle.

Implementation

Now, it is time for some hands-on lab. To implement this solution, we are going to use the following Dockerfile:

Source: Made by the author.

I recommend taking time to read each stage of this Multi-stage Dockerfile; after doing that, you will see that it contains 3 following stages:

  • Builder (Lines 1–3): In this stage, you will need to add or change the proper configuration to have your Java application compiled and built, depending on which application you are using (Gradle or Maven) this stage will be changed. For this tutorial, we are using a maven project; that’s why also the FROM instruction mentions a maven image.
  • Customjre (Lines 6–23): In this stage, we install and create the Java 17 runtime which will be used later to run your Java application, here you can also install/add any modules you will need for your custom runtime (default ones are being installed).
  • Final image (Lines 25–30): In this stage, the jar file generated in the first stage will be copied to be used and executed by the Java 17 runtime, which was previously copied to run your application.

If you want to check about vulnerabilities of this final image based on Scratch, you can use the following tool to scan it, the tool is called trivy. You can get additional information about it here.

Conclusion

To sum up, the use of multi-stage Dockerfile and scratch images gives significant benefits, such as optimizing image size and enhancing deployment efficiency and security through minimalistic containerization, resulting in a lightweight and purpose-specific final image. In addition, implementing this solution allows you to save cost if you store your images in a registry as an ECR (Elastic Container Registry).

References

--

--