Multistage Docker Builds

Haaris SayyedHaaris Sayyed
4 min read

In today's fast-paced development environment, efficiency and optimization are key. One powerful technique to achieve both when working with Docker is the use of multistage builds. This blog post will explore what multistage builds are and demonstrate how to implement them by considering Spring Boot application as example.

What is a Multistage Build?

A multistage build is a method of building Docker images that allows you to use multiple intermediate images to optimize your final build. This technique helps in:

  1. Reducing the size of the final image by only including necessary dependencies.

  2. Improving build performance by leveraging caching and reusing layers.

  3. Simplifying the build process by separating the build and runtime environments.

Why Use Multistage Builds?

Multistage builds are particularly useful for applications like Spring Boot, which typically have distinct build and runtime requirements. For instance, you might need a large JDK to compile your application but only a lightweight JRE to run it. Multistage builds let you handle this efficiently.

Setting Up Your Spring Boot Application

Let's start by setting up a simple Spring Boot application. If you already have one, you can skip this step.

Initialize a Spring Boot Project:

Use the Spring Initializer to generate a new Spring Boot project with the necessary dependencies. For this example, we'll use Spring Web.

$ curl https://start.spring.io/starter.zip \
    -d dependencies=web \
    -d name=demo \
    -d packageName=com.example.demo \
    -d javaVersion=17 \
    -d type=maven-project \
    -o demo.zip
$ unzip demo.zip -d demo
$ cd demo

Create a Simple REST Controller:

Create a new file src/main/java/com/example/demo/HelloController.java with the following content:

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello, World!";
    }
}

Build the Project:

Build the project using Maven or Gradle. Here we'll use Maven.

./mvnw clean package

Writing the Singlestage Dockerfile

let's create a singlestage Dockerfile.

Create a Dockerfile in the project root:

FROM openjdk:17
COPY target/demo-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Build the Docker Image:

$ docker build -t springboot-demo .

We can see that the image took 6.0 s to complete the build process.

Check size of image:

Writing the Multistage Dockerfile

let's create a multistage Dockerfile.

Create a Dockerfile in the project root and name it as Dockerfile.Multistage:

# Stage 1: Build
FROM maven:3.8.4-openjdk-17-slim AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
#
# Stage 2: Run
FROM gcr.io/distroless/java17-debian12
WORKDIR /app
COPY --from=build /app/target/demo-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
CMD ["app.jar"]

Build the Docker Image:

 $ docker build -f Dockerfile.Multistage -t springboot-demo-multistage .

We can see that the image took 1.9 s to complete the build process.

Check size of image:

Explanation of the Multistage Dockerfile

Let’s break down each part of the Dockerfile to understand what it does:

Stage 1: Build

# Stage 1: Build
FROM maven:3.8.4-openjdk-17-slim AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
  • FROM maven:3.8.4-openjdk-17-slim AS build: Specifies the base image for the build stage. We use a Maven image with JDK 17 to compile the application.

  • WORKDIR /app: Sets the working directory inside the container.

  • COPY pom.xml .: Copies the pom.xml file to the working directory.

  • COPY src ./src: Copies the source code to the working directory.

  • RUN mvn clean package -DskipTests: Runs the Maven build command inside the container to package the application, skipping tests for faster builds.

Stage 2: Run

# Stage 2: Run
FROM gcr.io/distroless/java17-debian12
WORKDIR /app
COPY --from=build /app/target/demo-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
CMD ["app.jar"]
  • FROM gcr.io/distroless/java17-debian12: This line specifies the base image for the runtime stage. We're using a lightweight image to run the application.

  • WORKDIR /app: Sets the working directory inside the container.

  • COPY --from=build /app/target/demo-0.0.1-SNAPSHOT.jar app.jar: Copies the JAR file from the build stage.

  • EXPOSE 8080: Exposes port 8080 for the application.

  • CMD ["app.jar"]: Provides the default command and arguments for the container.

Benefits of Multistage Builds

  • Reduced Image Size: By using separate stages, the final image only contains the necessary runtime environment and the application, significantly reducing its size.

  • Improved Security: Smaller images reduce the attack surface by including only essential components.

  • Faster Builds: Leveraging caching in intermediate stages speeds up the build process.

Conclusion

As we can observe from the result images present in the blog, using multistage Dockerfile instead of a conventional Dockerfile has reduced the image size by nearly 50% (i.e from 492 MB to 246 MB) and has reduced the build time by nearly 33.33% (i.e from 6.0s to 1.9s)

Multistage builds are a powerful feature in Docker that can significantly optimize the build and deployment process of your applications. By separating the build and runtime environments, you can create smaller, more efficient images that are easier to maintain and deploy. Give it a try with your next project and experience the benefits firsthand!

Happy containerizing!

Github Link :https://github.com/Haaris-Sayyed/springboot-multistage-docker-build-demo

Salute - Captain America GIF - Captain America Salute Chris Evans GIFs

0
Subscribe to my newsletter

Read articles from Haaris Sayyed directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Haaris Sayyed
Haaris Sayyed