DEV Community

Cover image for Optimizing Docker Image Size for Java Applications: Multi-Stage Builds & JLink Explained
Subbu Tech Tutorials
Subbu Tech Tutorials

Posted on

Optimizing Docker Image Size for Java Applications: Multi-Stage Builds & JLink Explained

Introduction:
In the fast-paced world of application deployment, optimizing Docker images is essential for enhancing efficiency and scalability. Large Docker images can slow build times, increase storage costs, and complicate deployments.

This blog explores practical techniques for optimizing Docker images, focusing on multi-stage builds and JLink. I will use the Spring PetClinic application as a case study to demonstrate how to reduce image sizes while ensuring they remain production-ready significantly.

Whether your goal is to accelerate CI/CD pipelines or create more secure deployments, this guide will help you achieve lightweight, efficient containers tailored to your application’s needs.

Method 1: The Original Dockerfile (Before Optimization)
Our initial Dockerfile is quite simple. It utilizes Maven to build the application and includes all the necessary dependencies for compilation and runtime. Although this approach works, it results in a large image because it incorporates unnecessary build tools and dependencies in the final runtime environment.

Here is an example of what the initial Dockerfile might look like:

# Initial Dockerfile: Build and Run in One Step (Non-Optimized)
FROM maven:3.9.4-eclipse-temurin-17-alpine

WORKDIR /app

# Copy source code and build the application
COPY . .
RUN mvn clean package -DskipTests

# Run the application
ENTRYPOINT ["java", "-jar", "target/spring-petclinic-*.jar"]
Enter fullscreen mode Exit fullscreen mode

Image description

This method generates a large image (approximately 400–500MB) since it includes Maven and all build dependencies that are not required for running the application in production.

Image description

Method 2: Optimizing the Dockerfile Using Multi-Stage Builds

  1. Using a Multi-Stage Build: We divided the build and runtime stages to retain only what is necessary for the final image.
  2. Caching Dependencies: We first copied the pom.xml file to cache dependencies, which speeds up future builds.
  3. Isolating Build Artifacts: We included only the compiled JAR file in the runtime image, omitting unnecessary build files.

These steps eliminated Maven and build-related files from the final image, resulting in a more efficient and smaller Docker image.

# Use a Maven image with JDK 17 for the build stage
FROM maven:3.9.4-eclipse-temurin-17-alpine AS build

WORKDIR /app

# Copy the pom.xml first and download the dependencies
COPY pom.xml ./
RUN mvn dependency:go-offline -B

# Copy the source code after dependencies are cached
COPY src ./src

# Package the application
RUN mvn clean package -DskipTests -Ddockerfile.skip=true

# Use JDK 17 for the runtime stage
FROM eclipse-temurin:17-jdk-alpine AS runtime

WORKDIR /app

# Copy the built JAR from the build stage
COPY --from=build /app/target/spring-petclinic-*.jar /app/app.jar

# Define the entry point
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
Enter fullscreen mode Exit fullscreen mode

Image description

Image description

Implementing these changes successfully reduced the image size from 518 MB to 396 MB. The final runtime image now includes only the essential Java Development Kit (JDK) and the compiled application JAR, eliminating the unnecessary overhead from Maven and other build dependencies.

Method 3: Optimizing Your Dockerfile for Advanced Performance

Multi-Stage Builds for Creating a Lightweight Production Image with a Minimal Java Runtime

Here’s a clearer version of the text:

“Let’s examine the optimized Dockerfile that utilizes multi-stage builds.”

# Stage 1: Build Stage
FROM maven:3.8.5-openjdk-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests

# Stage 2: Create minimal Java runtime with JLink
FROM eclipse-temurin:17-jdk-alpine AS jlink
RUN $JAVA_HOME/bin/jlink \
    --module-path $JAVA_HOME/jmods \
    --add-modules java.base,java.logging,java.xml,java.naming,java.sql,java.management,java.instrument,jdk.unsupported,java.desktop,java.security.jgss \
    --output /javaruntime \
    --compress=2 --no-header-files --no-man-pages

# Stage 3: Final Stage
FROM alpine:3.17
WORKDIR /app
COPY --from=jlink /javaruntime /opt/java-minimal
ENV PATH="/opt/java-minimal/bin:$PATH"
COPY --from=build /app/target/*.jar /app/app.jar

EXPOSE 8081
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
Enter fullscreen mode Exit fullscreen mode

Breaking Down the Steps for Optimization:
This Dockerfile efficiently uses a multi-stage build for a Spring Boot application, minimizing image size.

1. Build Stage:

  • Base: maven:3.8.5-openjdk-17
  • Purpose: To download dependencies and compile the application while caching dependencies to speed up future builds.

2. JLink Runtime Optimization:

  • Base: eclipse-temurin:17-jdk-alpine
  • Purpose: To create a minimal Java runtime using jlink that includes only the necessary modules while compressing and removing unneeded files to keep it small.

3. Final Stage:

  • Base: alpine:3.17
  • Purpose: To combine the minimal Java runtime with the application JAR, creating a lightweight and production-ready image.

Image description

Key Advantages of This Optimization

  1. Smaller Image Size: The size was reduced from approximately 518 MB to just 128 MB.
  2. Faster Deployment and Scaling: Smaller images load faster, accelerating deployment times, particularly in cloud environments.
  3. Reduced Attack Surface: By removing unnecessary tools and libraries, the optimized image becomes more secure and less vulnerable to attacks.

Image description

The Results: Comparison Before and After Optimization

  • Initial Image Size: ~The Dockerfile, when using Maven without optimization, has a size of 518 MB.
  • Optimized Image Size: ~The size is 128 MB when using the multi-stage Dockerfile with JLink.

To determine the image associated with the running petclinic-test container, use the following docker inspect command:

docker inspect --format='{{.Config.Image}}' petclinic-test
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can use the command docker ps along with the — filter option to specifically target your container:

docker ps --filter "name=petclinic-test" --format "{{.Image}}"
Enter fullscreen mode Exit fullscreen mode

Image description

Image description

Access the PetClinic application using your web browser:

Image description

Image description

Image description

Image description

Image description

Image description

Image description

Conclusion:
Optimizing Docker images goes beyond simply reducing their size; it involves creating production-ready containers that are fast, secure, and cost-effective. Techniques such as using multi-stage builds and creating minimal runtimes with JLink can significantly decrease image sizes, improve build performance, and reduce vulnerabilities.

For example, with the Spring PetClinic application, we achieved a size reduction from 518MB to 128MB, demonstrating the clear benefits of these strategies. Adopting these best practices can accelerate your deployments, save resources, and enhance security in your production environments. Start optimizing today to unlock Docker's full potential!

What are your thoughts on this article? Feel free to share your opinions in the comments below — or above, depending on your device! If you enjoyed the story, please consider supporting me by clapping, leaving a comment, and highlighting your favorite parts.

Visit subbutechops.com to explore the fascinating world of technology and data. Get ready for more exciting content. Thank you, and happy learning!

Top comments (0)