SpringBoot Dockerfile Diagnostics: Essential Dockerfile Tweaks for optimizing an image

Lakshyajit Laxmikant
6 min readFeb 24, 2024

In the world of software development, efficiency and optimization are not just buzzwords; they are essential practices that can significantly enhance performance and reduce resource consumption. This is especially true when deploying Spring Boot applications using Docker, where every byte and every second counts towards achieving a lean, fast, and reliable service. Let’s dive into how you can optimize your Dockerfiles for Spring Boot applications, transforming a standard Dockerfile into an optimized version for better performance and smaller image sizes. For those who are unaware or have little knowledge about what docker is,

Docker is a containerization platform that allows developers to package their applications and all of their dependencies into a standardized unit called a container. These containers can then be easily distributed and deployed across different environments, from development to production, without any changes.

For this example we’ll use the good old spring initializer website: https://start.spring.io/ to create a simple spring web project. Below is the screenshot of the dependencies and versions used:

Next you can import the project into your preferred IDE. For simplicity purposes I defined only one REST endpoint inside the app which will return a message when that endpoint is hit. I will let you know why I chose not to include any other dependencies or functionalities into this application in a bit. Here is the code for my endpoint:

@RestController
public class HelloWorldController {

@GetMapping("/hello")
public ResponseEntity<?> getResponse() {
HashMap<String, String> response = new HashMap<>();
response.put("message", "Hello World!");
return ResponseEntity.status(200).body(response);
}
}

Once this is done, we can build it using any IDE or manually by running the following command in the project root:

./gradlew build -x test

Now let’s verify that our simple endpoint is working as intended by running the application. Run the application using the following command and hit the “/hello” endpoint in a browser(or through a terminal).

./gradlew bootRun

Here is a screenshot of the response my endpoint returns:

Alright, we are all set with setting up our application. Next let’s build and run it as Docker container. Create a Dockerfile in the project root. First let’s keep things simple and create a very basic Dockerfile. For this example we’ll be using Java version 17 from the amazoncorretto:17-alpine3.17 base image.

Note: You can use any other base image version of your preferrence. I used amazoncorretto because I have used it previously in several other projects and it works just fine.

Here is the code for the 1st version of our Dockerfile:

# Step 1: Use Amazon Corretto 17 with Alpine Linux as the base image for building the application
FROM amazoncorretto:17-alpine3.17 as build

# Set the working directory in the container
WORKDIR /workspace/app

# Copy the Gradle wrapper and build files to the container
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY src src

# Grant execution permissions to the Gradle wrapper and build the application
RUN chmod +x ./gradlew && ./gradlew build -x test

# Step 2: Use Amazon Corretto 17 with Alpine Linux as the runtime base image
FROM amazoncorretto:17-alpine3.17

# Argument for passing the JAR file name
ARG JAR_FILE=/workspace/app/build/libs/*.jar

# Copy the JAR file from the build stage to the /app directory in the image
COPY --from=build ${JAR_FILE} app.jar

# Command to run the Spring Boot application
ENTRYPOINT ["java","-jar","/app.jar"]

As you can see the file is pretty self explanatory — We define our base image, then our image directory. Finally we copy the respective files into the image, build it and then define the ENTRYPOINT or RUN command. Now let’s build this docker image with the help of the above Dockerfile. In the project root, run the following command —

# Build your Docker image
docker build -t springboot-docker .

This will build our docker image out of the Dockerfile that we created previously. Once this is done, let’s verify the image size by running the command docker images in our terminal. Here is a screenshot of the result of this command for me:

Woah! Are you seeing this?! we just wrote a simple endpoint and did not include any other dependency in our project, yet our image size is a whopping 306MB!. Now some of the Java devs might be thinking well everyone knows unlike other frameworks/languages like Golang, Node+Express.js, Java is a bit memory intensive — that’s true actually, but as the age old Computer Science saying goes — “we can definitely do better!” Now let’s optimize our Dockerfile. Here is how the optimized version looks like —

FROM --platform=linux/amd64 amazoncorretto:17-alpine3.17 as corretto-jdk

# required for strip-debug to work
RUN apk add --no-cache binutils

# Build small JRE image
RUN $JAVA_HOME/bin/jlink \
--verbose \
--add-modules ALL-MODULE-PATH \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /customjre

# main app image
FROM alpine:latest
ENV JAVA_HOME=/jre
ENV PATH="${JAVA_HOME}/bin:${PATH}"

# copy JRE from the base image
COPY --from=corretto-jdk /customjre $JAVA_HOME

# Add app user
ARG APPLICATION_USER=appuser
RUN adduser --no-create-home -u 1000 -D $APPLICATION_USER

# Configure working directory
RUN mkdir /app && \
chown -R $APPLICATION_USER /app

USER 1000

COPY --chown=1000:1000 build/libs/springboot-docker-0.0.1-SNAPSHOT.jar /app/app.jar
WORKDIR /app

EXPOSE 8080
ENTRYPOINT [ "/jre/bin/java", "-Dspring.profiles.active=prod", "-jar", "/app/app.jar" ]

Before going into the details of the optimizations, let’s run the docker build command again and see how it goes. Here is another screenshot for the update image size:

That’s 118MB now! More than 60% reduction in size which is definitely a good start. Now let’s discuss about the optimizations in detail:

Custom JRE Creation: Using jlink to create a minimal JRE tailored for the application reduces the image size significantly. By stripping debug information, removing man pages, and compressing the JRE, we ensure that only the necessary runtime components are included.

Security Enhancements: Switching to a non-root user (appuser) improves security by limiting the permissions of the application process. This is a best practice for running applications in Docker containers.

Base Image Optimization: Starting with the alpine image minimizes the size of the runtime image. Alpine Linux is renowned for its minimal footprint and security profile, making it an ideal choice for production environments.

Efficient Layering: By carefully organizing copy commands and minimizing the number of layers, the Dockerfilereduces build time and potential attack surfaces.

Note: Since I am running docker on a mac, I had to include the --platform flag while importing the base image.

There are quite a few other optimizations possible as well, but those are out of the scope of this article. I leave it to the readers of this article to explore further and make the necessary changes as per their own business or technical requirements. That’s it for now.
Cheers!

--

--

Lakshyajit Laxmikant

A tech-geek, curious creature, willing to learn new technologies to build interesting and intelligent systems...