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"]
The Multi-stage Build
This line:
FROM amazoncorretto:25-alpine-full AS build
and this line:
FROM alpine:3.22 AS final
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
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
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
does the following:
Extract the fat JAR file (in this project, the built JAR file name is
app.jar)Use
jdepsto 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
jlinkto create our own JRE image. Note that we also includejdk.crypto.echere, 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
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"]
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)