How to build and configure custom JRE for Java Spring web application in Docker Image/containers

I am building my Java API server into a Docker image. I am making my own custom JRE using this:
RUN jdeps \
--ignore-missing-deps \
-q \
--multi-release 21 \
--print-module-deps \
--class-path build/lib/* \
build/libs/*.jar > jre-deps.info

RUN jlink \
--compress 2 \
--strip-java-debug-attributes \
--no-header-files \
--no-man-pages \
--output jre \
--add-modules $(cat jre-deps.info)
RUN jdeps \
--ignore-missing-deps \
-q \
--multi-release 21 \
--print-module-deps \
--class-path build/lib/* \
build/libs/*.jar > jre-deps.info

RUN jlink \
--compress 2 \
--strip-java-debug-attributes \
--no-header-files \
--no-man-pages \
--output jre \
--add-modules $(cat jre-deps.info)
Then copying into a scratch container image so there is nothing else. I would like to keep it this way to minimize attack vectors and keep the image size on the smaller side as much as possible. I am unfamiliar with Java applications, this is the first time I'm building an application with Java and I'd like to learn best practices. What are some common arguments to pass into the JVM when you run your application in a container? I read briefly about how you should set the heap sizes and put on container support to prevent the JVM from adjusting the max heap size when running in a container. Is this advised? What else would you run in addition to the container support and heap size arguments? The goal is to keep attack vectors small while still maintaining good performance. At the end, this is what I have for my final image:
############ FINAL PRODUCTION IMAGE ############
FROM scratch

ARG JAR_NAME
WORKDIR /

COPY --from=jdk /jre /jre

COPY "./server/build/libs/${JAR_NAME}" "./${JAR_NAME}"

ENV JAVA_HOME="/jre"
ENV PATH="${JAVA_HOME}/bin:${PATH}"
ENV APPLICATION_EXECUTABLE="${JAR_NAME}"

EXPOSE 8080
EXPOSE 80
EXPOSE 443

CMD "/jre/bin/java" \
"-XX:+UseContainerSupport" \
"-XX:MaxRAMPercentage=75" \
"-Djava.security.egd=file:/dev/./urandom" \
"-jar" "./${APPLICATION_EXECUTABLE}"
############ FINAL PRODUCTION IMAGE ############
FROM scratch

ARG JAR_NAME
WORKDIR /

COPY --from=jdk /jre /jre

COPY "./server/build/libs/${JAR_NAME}" "./${JAR_NAME}"

ENV JAVA_HOME="/jre"
ENV PATH="${JAVA_HOME}/bin:${PATH}"
ENV APPLICATION_EXECUTABLE="${JAR_NAME}"

EXPOSE 8080
EXPOSE 80
EXPOSE 443

CMD "/jre/bin/java" \
"-XX:+UseContainerSupport" \
"-XX:MaxRAMPercentage=75" \
"-Djava.security.egd=file:/dev/./urandom" \
"-jar" "./${APPLICATION_EXECUTABLE}"
43 Replies
JavaBot
JavaBot2mo ago
This post has been reserved for your question.
Hey @DaMango! Please use /close or the Close Post button above when your problem is solved. Please remember to follow the help guidelines. This post will be automatically marked as dormant after 300 minutes of inactivity.
TIP: Narrow down your issue to simple and precise questions to maximize the chance that others will reply in here.
dan1st
dan1st2mo ago
Then copying into a scratch container image so there is nothing else. I would like to keep it this way to minimize attack vectors and keep the image size on the smaller side as much as possible.
I don't think a jlink'ed package would work with FROM scratch as it needs some things present on the system - at least libc
Gareth Rader
Gareth RaderOP2mo ago
it seems to run at least?
dan1st
dan1st2mo ago
oh, it does?
Gareth Rader
Gareth RaderOP2mo ago
ya I think the JRE includes various DLLs let me look
dan1st
dan1st2mo ago
it would be .so files but I didn't think it would include libc if you really want to have it minimal, you could use native-image
Gareth Rader
Gareth RaderOP2mo ago
ARG JAR_NAME=app.jar

FROM openjdk:21 AS jdk
ARG JAR_NAME

COPY "./server/build/libs/${JAR_NAME}" "./build/libs/${JAR_NAME}"

RUN jdeps \
--ignore-missing-deps \
-q \
--multi-release 21 \
--print-module-deps \
--class-path build/lib/* \
build/libs/*.jar > jre-deps.info

RUN jlink \
--compress 2 \
--strip-java-debug-attributes \
--no-header-files \
--no-man-pages \
--output jre \
--add-modules $(cat jre-deps.info)


############ FINAL PRODUCTION IMAGE ############
FROM scratch

ARG JAR_NAME

WORKDIR /

COPY --from=jdk /jre /jre

COPY "./server/build/libs/${JAR_NAME}" "./${JAR_NAME}"

#include bash + other commandline programs
COPY --from=jdk /bin /bin
COPY --from=jdk /usr/bin /usr/bin
COPY --from=jdk /lib64 /lib64

ENV JAVA_HOME="/jre"
ENV PATH="${JAVA_HOME}/bin:${PATH}"
ENV APPLICATION_EXECUTABLE="${JAR_NAME}"

EXPOSE 8080
EXPOSE 80
EXPOSE 443

CMD "/jre/bin/java" \
"-XX:+UseContainerSupport" \
"-XX:MaxRAMPercentage=75" \
"-Djava.security.egd=file:/dev/./urandom" \
"-jar" "./${APPLICATION_EXECUTABLE}"
ARG JAR_NAME=app.jar

FROM openjdk:21 AS jdk
ARG JAR_NAME

COPY "./server/build/libs/${JAR_NAME}" "./build/libs/${JAR_NAME}"

RUN jdeps \
--ignore-missing-deps \
-q \
--multi-release 21 \
--print-module-deps \
--class-path build/lib/* \
build/libs/*.jar > jre-deps.info

RUN jlink \
--compress 2 \
--strip-java-debug-attributes \
--no-header-files \
--no-man-pages \
--output jre \
--add-modules $(cat jre-deps.info)


############ FINAL PRODUCTION IMAGE ############
FROM scratch

ARG JAR_NAME

WORKDIR /

COPY --from=jdk /jre /jre

COPY "./server/build/libs/${JAR_NAME}" "./${JAR_NAME}"

#include bash + other commandline programs
COPY --from=jdk /bin /bin
COPY --from=jdk /usr/bin /usr/bin
COPY --from=jdk /lib64 /lib64

ENV JAVA_HOME="/jre"
ENV PATH="${JAVA_HOME}/bin:${PATH}"
ENV APPLICATION_EXECUTABLE="${JAR_NAME}"

EXPOSE 8080
EXPOSE 80
EXPOSE 443

CMD "/jre/bin/java" \
"-XX:+UseContainerSupport" \
"-XX:MaxRAMPercentage=75" \
"-Djava.security.egd=file:/dev/./urandom" \
"-jar" "./${APPLICATION_EXECUTABLE}"
That is the full dockerfile. edit: The below is more for debugging capabilities I add
COPY --from=jdk /bin /bin
COPY --from=jdk /usr/bin /usr/bin
COPY --from=jdk /lib64 /lib64
COPY --from=jdk /bin /bin
COPY --from=jdk /usr/bin /usr/bin
COPY --from=jdk /lib64 /lib64
because sometimes i want to inspect with bash and otherwise there is no terminal in the container image. However, the image also runs without including these, which I'm planning to take out for the production image
dan1st
dan1st2mo ago
oh I didn't see these yeah then it might not be as minimal lol
Gareth Rader
Gareth RaderOP2mo ago
The image works without the /lib64 and those lines
dan1st
dan1st2mo ago
What does the application do?
Gareth Rader
Gareth RaderOP2mo ago
its a spring boot web application
dan1st
dan1st2mo ago
ok
Gareth Rader
Gareth RaderOP2mo ago
i haven't programmed anything, i wanted to get the container image pipeline set up so I can do automated integration testing a bit easier
dan1st
dan1st2mo ago
Does it serve HTTP request with that image?
Gareth Rader
Gareth RaderOP2mo ago
I haven't tried yet haha I think my issue was my gradle build file wasn't including the netty server or tomcat server
dan1st
dan1st2mo ago
try it before thinking about JVM arguments
Gareth Rader
Gareth RaderOP2mo ago
that seems like it would be useful in the container image?
dan1st
dan1st2mo ago
Does it work on your host?
Gareth Rader
Gareth RaderOP2mo ago
no it runs and exits with exit code 0
dan1st
dan1st2mo ago
then you should get that working before the Docker image
Gareth Rader
Gareth RaderOP2mo ago
is shadowJar a good tool to use? I was looking to create a fatJar
dan1st
dan1st2mo ago
that might be also something that can happen in the Docker image because of using FROM scratch
Gareth Rader
Gareth RaderOP2mo ago
or something that is a little more self-contained
dan1st
dan1st2mo ago
If you really want to build a single JAR file that contains the application, it's an option but I wouldn't recommend that approach
Gareth Rader
Gareth RaderOP2mo ago
what would you recommend
dan1st
dan1st2mo ago
One of the following: - Use Spring's tooling for packaging the app as a JAR - Copy the application and dependency JAR in one directory and run it from there - you could also use an exploded format - If you really want a Docker image that's as minimal as possible (at the cost of creating that file taking a few minutes and quite a bit more CPU/memory), you can try native-image. With that (and static linking with musl), you might even be able to make the last stage look like this (while Spring supports native-image, idk whether it works with static linking):
FROM scratch
# yes, that's all for the last stage if you do static linking with musl
COPY --from=builder /work/build/app /app
ENTRYPOINT ["/app"]
FROM scratch
# yes, that's all for the last stage if you do static linking with musl
COPY --from=builder /work/build/app /app
ENTRYPOINT ["/app"]
but before you work on packages, you should at least have a running application that is at least able to serve HTTP requests. It doesn't need to be able to do anything useful/have any custom code but you need some way of testing whether it actually works and for JVM arguments, you should probably tune them once you see what the application actually needs
Gareth Rader
Gareth RaderOP2mo ago
so when I just run a ./gradlew clean build with the spring boot plugin it produces a few different jar files. Do you know what these usually might be?
No description
dan1st
dan1st2mo ago
important JVM arguments will probably be the heap size and the selection of which GC to use
Gareth Rader
Gareth RaderOP2mo ago
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.6'
id 'io.spring.dependency-management' version '1.1.7'
id 'com.google.protobuf' version '0.9.4'
}
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.6'
id 'io.spring.dependency-management' version '1.1.7'
id 'com.google.protobuf' version '0.9.4'
}
dan1st
dan1st2mo ago
the -all.jar is basically a fat JAR containing everything necessary I think the normal one is probably only the application code and idk the difference between the normal JAR and the -plain.jar I guess it first created the -plain.jar and then the Spring plugin did some transformation but I normally use Maven lol
Gareth Rader
Gareth RaderOP2mo ago
ah gotcha sorry i'm still learning java so i am still trying to learn about how everything fits together that is java specific i'll make a small http endpoint to test if it is working i think building normally includes a tomcat server
dan1st
dan1st2mo ago
What exactly? Maven and Gradle are build tools that are mostly (but not solely) used for Java/JVM applications. The Spring plugin is spring specific I think that even without an endpoint, it should serve HTTP requests (you'd just get 404s) at least as long as you have the web started added
Gareth Rader
Gareth RaderOP2mo ago
well things like building JARs, JVM configuration, configuring tomcat/netty since i'm used to just building binaries with shared libraries or python/interpretted languages @dan1st | Daniel tomcat is the default for spring boot applications, would you ever recommend using netty over tomcat for spring boot apps? or what have you seen there
dan1st
dan1st2mo ago
I think netty is the default for reactive Spring Boot Web applications but I could be wrong on that
Gareth Rader
Gareth RaderOP2mo ago
oh i think you are right
dan1st
dan1st2mo ago
These are all JVM specific and other languages do their own thing
Gareth Rader
Gareth RaderOP2mo ago
i don't think i'm using reactive spring boot?
dan1st
dan1st2mo ago
if there's nothing saying "reactive" or "reactor" in your build.gradle, you aren't
Gareth Rader
Gareth RaderOP2mo ago
i think i just put down the spring boot gradle plugin and the web app starter
dan1st
dan1st2mo ago
and it will be more convenient without reactive stuff if you need the scalability, you can enable virtual threads
Gareth Rader
Gareth RaderOP2mo ago

implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
And plugins:
id 'org.springframework.boot' version '3.5.6'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.springframework.boot' version '3.5.6'
id 'io.spring.dependency-management' version '1.1.7'
dan1st
dan1st2mo ago
yeah that's non-reactive
JavaBot
JavaBot2mo ago
💤 Post marked as dormant
This post has been inactive for over 300 minutes, thus, it has been archived. If your question was not answered yet, feel free to re-open this post or create a new one. In case your post is not getting any attention, you can try to use /help ping. Warning: abusing this will result in moderative actions taken against you.

Did you find this page helpful?