DEV Community

Linh Nguyen
Linh Nguyen

Posted on

Create a Smaller Docker Image for Our Spring Boot Application

The original article is hosted on my website here, so for my first article, I'll keep it short, with step-by-step explanation.

TL;DR

You can check the sample repository, or here is the full Dockerfile:

FROM amazoncorretto:25-alpine-full AS build

WORKDIR /usr/src/project

ENV JAVA_VERSION=25
ENV APP_NAME=app.jar
ENV DEPS_FILE=deps.info

COPY pom.xml mvnw ./
COPY .mvn/ .mvn/
RUN chmod +x mvnw

RUN ./mvnw dependency:go-offline

COPY src/ src/

RUN ./mvnw clean package -DskipTests

RUN jar xf target/${APP_NAME}

RUN jdeps  \
    --ignore-missing-deps  \
    -q --recursive  \
    --multi-release ${JAVA_VERSION} \
    --print-module-deps  \
    --class-path 'BOOT-INF/lib/*' \
    target/${APP_NAME} > ${DEPS_FILE}

RUN jlink \
    --add-modules $(cat ${DEPS_FILE}),jdk.crypto.ec \
    --strip-java-debug-attributes  \
    --compress 2  \
    --no-header-files  \
    --no-man-pages \
    --output /jre-minimalist

FROM alpine:3.22 AS final

ENV JAVA_HOME=/opt/java/jre-minimalist
ENV PATH=$JAVA_HOME/bin:$JAVA_HOME/lib:$PATH
ENV USER=springuser
ENV GROUP=springgroup
ENV WORKDIR=app
ENV APP_NAME=app.jar

COPY --from=build /jre-minimalist $JAVA_HOME

RUN addgroup -S ${GROUP} \
    && adduser -S ${USER} -G ${GROUP} \
    && mkdir -p /app \
    && chown -R ${USER}:${GROUP} /${WORKDIR}

COPY --from=build /usr/src/project/target/${APP_NAME} /${WORKDIR}/

WORKDIR /${WORKDIR}

USER ${USER}

ENTRYPOINT ["java", \
    "-XX:+UseCompactObjectHeaders", \
    "-XX:MaxRAMPercentage=75.0", \
    "-XX:InitialRAMPercentage=50.0", \
    "-XX:MaxMetaspaceSize=512m", \
    "-jar", \
    "app.jar"]
Enter fullscreen mode Exit fullscreen mode

The Multi-stage Build

This line:

FROM amazoncorretto:25-alpine-full AS build
Enter fullscreen mode Exit fullscreen mode

and this line:

FROM alpine:3.22 AS final
Enter fullscreen mode Exit fullscreen mode

indicate that our build uses multiple stages. This is an effective strategy that helps with our build by, for example, caching multiple RUN commands during the build phase, and (most importantly) helping reduce the final image size.

The Maven Wrapper

You can see this part:

COPY pom.xml mvnw ./
COPY .mvn/ .mvn/
RUN chmod +x mvnw
Enter fullscreen mode Exit fullscreen mode

Both the .mvn folder and mvnw file (and possibly mvnw.cmd if you're using Windows) are from the Maven wrapper, which often accompanies your project if it is created using Spring Initializr. We'll be using the Maven wrapper with no need to use a Maven Docker image for our build phase.

The Maven Build

The build stage:

RUN ./mvnw dependency:go-offline

COPY src/ src/

RUN ./mvnw clean package -DskipTests
Enter fullscreen mode Exit fullscreen mode

This will help with caching: as long as there are no dependency changes, we can speed up subsequent builds with all dependencies downloaded and cached.

We'll be copying everything from the src folder and the pom.xml file (see above) and start our build. We can skip the tests by specifying -DskipTests (we can defer unit tests to feature branch builds).

The Custom JRE Image Creation

This part:

RUN jar xf target/${APP_NAME}

RUN jdeps  \
    --ignore-missing-deps  \
    -q --recursive  \
    --multi-release ${JAVA_VERSION} \
    --print-module-deps  \
    --class-path 'BOOT-INF/lib/*' \
    target/${APP_NAME} > ${DEPS_FILE}

RUN jlink \
    --add-modules $(cat ${DEPS_FILE}),jdk.crypto.ec \
    --strip-java-debug-attributes  \
    --compress 2  \
    --no-header-files  \
    --no-man-pages \
    --output /jre-minimalist
Enter fullscreen mode Exit fullscreen mode

does the following:

  • Extract the fat JAR file (in this project, the built JAR file name is app.jar)

  • Use jdeps to detect the required modules needed to run our Spring Boot application container and export the found module list to a file, in this case, deps.info.

  • Finally, use jlink to create our own JRE image. Note that we also include jdk.crypto.ec here, because my sample project uses HTTPS, and without this module, we cannot make HTTP requests to the application running in the container.

The Final Touches

This part:

COPY --from=build /jre-minimalist $JAVA_HOME
Enter fullscreen mode Exit fullscreen mode

means we are copying our custom JRE image from the build stage to our $JAVA_HOME folder.

The rest:

RUN addgroup -S ${GROUP} \
    && adduser -S ${USER} -G ${GROUP} \
    && mkdir -p /app \
    && chown -R ${USER}:${GROUP} /${WORKDIR}

COPY --from=build /usr/src/project/target/${APP_NAME} /${WORKDIR}/

WORKDIR /${WORKDIR}

USER ${USER}

ENTRYPOINT ["java", \
    "-XX:+UseCompactObjectHeaders", \
    "-XX:MaxRAMPercentage=75.0", \
    "-XX:InitialRAMPercentage=50.0", \
    "-XX:MaxMetaspaceSize=512m", \
    "-jar", \
    "app.jar"]
Enter fullscreen mode Exit fullscreen mode

simply performs some (not so) trivial tasks: creating a non-root user, copying the fat JAR file, specifying some JVM arguments, and voilà, the image is ready to be built!


For a better explanation of the whole process, please visit this article and leave your comments. I'd be happy to hear from you!

Top comments (0)