Hey there, fellow Docker enthusiast! š Today I want to chat about something that had bothered me in the past - those hefty Java Docker images. You know what Iām talking about, right? You create a simple āHello Worldā app and somehow end up with nearly half a TB of Docker image! To put it in perspective, Apple sells an upgrade of 250MB for about INR 20,000. Half a TB, if it were for Apple, would cost whooping INR 40,000. Thatās crazy cost to print a helloworld!!! Letās fix that together - optimize as much as possible.
Weāll start with this super simple Java program:
package hello1;
public class hello {
public static void main(String[] args) {
System.out.println("Greetings World! Let's optimize our java packaging today.");
}
}
Nothing fancy here - just greeting the world and letting everyone know weāre on an optimization mission today!
Letās check the actual size of our compiled app:
$ ls -la
total 16
drwxr-xr-x 5 sandeep staff 160 Apr 5 08:39 .
drwxr-xr-x 3 sandeep staff 96 Apr 5 00:16 ..
-rw-r--r--@ 1 sandeep staff 25 Apr 5 00:50 MANIFEST.MF
-rw-r--r-- 1 sandeep staff 1165 Apr 5 08:39 hello.jar
drwxr-xr-x 4 sandeep staff 128 Apr 5 08:39 hello1
$ ls -lh hello1
total 16
-rw-r--r-- 1 sandeep staff 467B Apr 5 16:38 hello.class
-rw-r--r--@ 1 sandeep staff 178B Apr 5 16:38 hello.java
Our entire application is only a few hundred bytes. The class file is 467 bytes and the source code is 178 bytes. Here comes the packaging overheads. The jar is 1165 bytes. Still reasonable compared to what follows. This tiny app will end up in a container hundreds of megabytes in size. Talk about overhead! š®
Most of us start here - grab a standard Java image and get things running:
FROM eclipse-temurin:21-jdk
WORKDIR /app
COPY hello.jar /app/
ENTRYPOINT ["java", "-jar", "hello.jar"]
Lets build an image
$ docker build . -t hello1-java
[+] Building 180.7s (8/8) FINISHED docker:desktop-linux
...
=> [1/3] FROM docker.io/library/eclipse-temurin:21-jdk@sha256:6634936b2e8d90ee16eeb94420d71cd5e36ca677a4cf795a9ee1ee6e94379988 177.3s
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello1-java latest 7104f6490ae9 About a minute ago 704MB
Yikes! Weāre looking at a 704MB+ image and more than 3 minutes of build time. Thatās like using a moving truck to deliver a postcard. š
A quick win is switching to Alpine Linux. Itās like the difference between checking a suitcase for a flight versus just bringing a backpack:
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY hello.jar /app/
ENTRYPOINT ["java", "-jar", "hello.jar"]
The results:
$ docker build . -t hello1-java
[+] Building 129.9s (8/8) FINISHED docker:desktop-linux
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello1-java latest c6d19d8c8076 2 minutes ago 550MB
Just like that, weāve shaved off about 150MB! Our image is now around 550MB. Not bad for a one-line change, right?
Think about it - weāre just running Java code, not writing it. So why bring the whole development kit? Letās switch from JDK to JRE:
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY hello.jar /app/
ENTRYPOINT ["java", "-jar", "hello.jar"]
Drumroll results!
$ docker build . -t hello1-java
[+] Building 39.2s (8/8) FINISHED docker:desktop-linux
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello1-java latest 9c43612a462f 4 seconds ago 282MB
Almost half gone - 2X ! Weāre down to about 282MB now. You donāt need to bring your entire toolbox when all that is needed is a screwdriver. š§ Great!
Before we go further with optimizations, letās make our container safer. Running as root in containers is a bit like leaving your car unlocked with the keys inside:
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY hello.jar /app/
# Create a regular user
RUN addgroup -S javauser && adduser -S -G javauser javauser && \
chown -R javauser:javauser /app
# Switch to that user
USER javauser
ENTRYPOINT ["java", "-jar", "hello.jar"]
This doesnāt reduce our image size and more importantly doesnāt add to the image size. But hey, security matters too!
Now for my favorite part! What if I told you we could create a custom Java runtime with just the parts our app actually needs? Thatās exactly what jlink
does:
# Stage 1: Build our custom JRE
FROM eclipse-temurin:21-jdk-alpine AS builder
# Create a minimal JRE with just what we need
RUN jlink \
--add-modules java.base \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /customjre
# Stage 2: Create our final slim image
FROM alpine:3.19
# Copy only the custom JRE
COPY --from=builder /customjre /opt/java
# Set up Java environment
ENV JAVA_HOME=/opt/java
ENV PATH="${JAVA_HOME}/bin:${PATH}"
WORKDIR /app
COPY hello.jar /app/
# Create a non-root user
RUN addgroup -S javauser && adduser -S -G javauser javauser && \
chown -R javauser:javauser /app /opt/java
USER javauser
# Run our app
ENTRYPOINT ["java", "-jar", "hello.jar"]
Drumrollss pleaseā¦
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello1-java latest ad42d514d4b7 7 seconds ago 132MB
$ docker build . -t hello1-java
[+] Building 44.9s (13/13) FINISHED docker:desktop-linux
This two-stage approach is like cooking in the kitchen but only bringing the finished dish to the table. We end up with just 132MB - a whopping 5X reduction from where we started! š
Did we go back to jdk? Yes we did! However, we still managed to end up with a small application image. How did that happen? Magic!
āA recipe has no soul. You as the cook must bring soul to the recipe.ā ā Thomas Keller
This journey wasnāt without a few facepalm moments:
--compress=9
with jlink (more is better, right?). Turns out it only accepts values 0-2, compress=<0,1,2>. Whoops! > [builder 2/2] RUN jlink --add-modules java.base --strip-debug --no-man-pages --no-header-files --compress=9 --vm=server --output /customjre:
0.156 Error: Invalid compression level 9
--no-fallback
to prevent jlink from including extra modules ājust in case.ā Threw unknown option. Not everything found on internet works. Check command line help or documentation before applying options. > [builder 2/2] RUN jlink --add-modules java.base --strip-debug --no-man-pages --no-header-files --compress=2 --vm=server --no-fallback --output /customjre:
0.131 Error: unknown option: --no-fallback
--vm=server
, āāno-man-pagesā didināt result in anything different. I expected the application image to be smaller though.Letās see what we accomplished:
Approach | Size | What We Saved |
---|---|---|
Standard JDK | ~700MB | Our starting point |
Alpine JDK | ~550MB | 20% smaller! |
Alpine JRE | ~280MB | 60% smaller! |
Custom JRE | ~130MB | 80% smaller or 5X! |
Finally, from a truck to a two-wheeler to send the postcard. Sounds right!
Hereās what Iāll remember for next time:
Iām thinking about:
What optimization tricks have you discovered? Drop me a comment (github) - Iād love to hear about your container-slimming adventures!
This post was written after a fun afternoon of Docker optimization. Coffee consumption: high. Image size: low. Just how I like it! ā