How to optimize Docker image using Multistage Build

How to optimize Docker image using Multistage Build

docker_multistage_build

Here in this article we will try to optimize a spring boot application Dockerfile using the Docker Multistage build feature and validate the application.

Test Environment

Fedora 41 Server
Docker v27.4.1

Traditional Build

A Dockerfile consist of instructions to build, package and run an application. These instructions are executed in the sequential order. In a traditional build all the build instructions related to downloading dependencies, compiling the source code, building the package are executed in a single build container which leads to a heavy and bulky image.

This bulky image with unnecessary file such as binaries and compiled source code increase security risk. This issue can be avoided by using the multi stage builds.

Multistage Build

Multi-stage builds introduce multiple stages in your Dockerfile, each with a specific purpose (eg. build environment and runtime environment). By separating the build environment from the final runtime environment, you can significantly reduce the image size and attack surface.

Here is a sample Dockerfile with multistage build in action using pseudo-code.

# Stage 1: Build Environment
FROM builder-image AS build-stage 
# Install build tools (e.g., Maven, Gradle)
# Copy source code
# Build commands (e.g., compile, package)

# Stage 2: Runtime environment
FROM runtime-image AS final-stage  
#  Copy application artifacts from the build stage (e.g., JAR file)
COPY --from=build-stage /path/in/build/stage /path/to/place/in/final/stage
# Define runtime configuration (e.g., CMD, ENTRYPOINT) 
  • The build environment uses a builder-image containing build tools needed to compile your application. It includes commands to install build tools, copy source code, and execute build commands.
  • The runtime environment uses a smaller base image suitable for running your application. It copies the compiled artifacts (a JAR file, for example) from the build stage. Finally, it defines the runtime configuration (using CMD or ENTRYPOINT) for starting your application.

If you are interested in watching video. Here is the YouTube video on the same step by step procedure outline below.

Procedure

Step1: Ensure Docker installed and running

As a first step ensure you have a working docker installation on your workstation. Check the status of the docker service if running as shown below.

sudo systemctl status docker.service

Step2: Clone Springboot application

Let’s now take a sample gradle based spring boot application by cloing the below repository.

git clone https://github.com/novicejava1/ldapdemo.git

Step3: Unoptimized Dockerfile

Here we will update the existing Dockerfile to build and execute our spring boot application from with the Container itself.

Change to ldapdemo directory and update the Dockerfile as shown below.

File: Dockerfile

# Stage: Build and Execute
FROM openjdk:17-jdk-alpine
WORKDIR /app
COPY . .
RUN ./gradlew build
ENTRYPOINT ["java","-jar","/app/build/libs/ldapdemo-0.0.1-SNAPSHOT.jar"]

Now let’s build the docker image using the below script.

./docker-build.sh

Here is the docker image that got created with a size of “601MB” which is huge. The drawbacks of having a large image size are they can causes delays in pulling the docker image and pose security risk.

docker images

Output:

REPOSITORY    TAG       IMAGE ID       CREATED              SIZE
ldapdemo      0.0.1     bfdf1cf976a0   About a minute ago   601MB

Step4: Optimize Dockerfile

Let’s now try to optimize our Dockerfile using Docker’s multistage feature wherein we will compile and build the jar package in the Stage 1 “Build environment”. We will then copy the jar file from Stage 1 using the “–from=builder” and copy to our Stage 2 “Runtime environment” at the required location and launch the application.

Here is the optimized Dockerfile.

File: Dockerfile

# Stage 1: Build environment
FROM openjdk:17-jdk-alpine AS builder
WORKDIR /app
COPY . .
RUN ./gradlew build

# Stage 2: Runtime environment
FROM openjdk:17-jdk-alpine AS runtime
WORKDIR /app
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring  
#  Copy application artifacts from the build stage (e.g., JAR file)
COPY --from=builder /app/build/libs/*.jar /app/ldapdemo.jar
# Define runtime configuration (e.g., CMD, ENTRYPOINT)
ENTRYPOINT ["java","-jar","/app/ldapdemo.jar"]

Now let’s build the docker image using the below script.

./docker-build.sh

As you can see the docker image size has been reduced to “356MB”.

docker images

Output:

REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
ldapdemo      0.0.1     350bcaebdd1c   43 seconds ago   356MB

Step5: Validate Application

Now we are ready to launch our optimized docker image application as shown below.

docker run -d -p 8080:8080 --name ldapdemo ldapdemo:0.0.1

We can validate the application using the below url.

curl http://localhost:8080/login

Hope you enjoyed reading this article. Thank you..