Hello Everyone and welcome to my session regarding docker. The slide deck and common commands have already been given to you, now let's do something hands on.
This project contains a simple hello world project (a simple server) which we are going to docker-ize! 🤩
FROM eclipse-temurin:21-jdk
LABEL authors="Shounak Bhalerao"
LABEL name="Dockerfile-0"
WORKDIR /app
EXPOSE 8080/tcp
COPY target/docker-spring-boot-optimization-tutorial-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
A simple docker file which selects a base image adds some label (ofc I want my name in the authors) and exposes the entrypoint. Now what could be the problem??
firstly, lets go over the size of this image:
This simple application, is taking half a GigaByte of storage!
That is not good. But what happened?
Turns out that the base image that we are using is problematic.
Images tagged with 'Alpine' are generally considered as the most lightweight images. mainly because all the bloat has been removed from them. Lets use an alpine image and check how what do we get:
FROM eclipse-temurin:21-jdk-alpine
LABEL authors="Shounak Bhalerao"
WORKDIR /app
COPY target/docker-spring-boot-optimization-tutorial-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
FROM eclipse-temurin:21-jre-alpine
LABEL authors="Shounak Bhalerao"
WORKDIR /app
COPY target/docker-spring-boot-optimization-tutorial-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
By simply changing our base image we have halved our container size!
Now lets get back to another problem, every time i make a change,
I need to manually run mvn clean package
to remove previous .jar
file created
and create a new one. Can i automate it without adding to the size of containers??
By adding STAGES using as
keyword, we divide our workflow between different images.
our first image will build the application while our second will run it. Hence ensuring
the final image does not have its size increased.
FROM maven:3.9.4-eclipse-temurin-17 AS build
WORKDIR /app
COPY . .
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre-alpine AS run
WORKDIR /app
COPY --from=build /app/target/docker-spring-boot-optimization-tutorial-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
Until now we have been working on these images (which are basically stripped down version of linux)
and using Root User. This can lead to other problems if any attack gets into our system. and as a developer
I know that docker run -it <IMAGE_NAME>
will provide me with interactive terminal (with root access if not secured) 😆
Hence lets add new use & group to our application. The following lines are important:
RUN addgroup -system appgroup && adduser -system appuser
RUN chown -R appuser:appgroup /app
Okay, so we all have been in a situation, where our PC suddenly
turned off and we lost our precious data in the way. Well docker has
a special ways to tell the application inside it that it is shutting down!
Awesome now we can let Spring boot know, that we got the shutdown command,
and quickly save all the tasks. This behaviour is clutch when you are working
with Faas or Serverless AWS Lambdas! (or even Spot Instances).
Different GC algorithms are available, and each has its strengths:
GC Algorithm | Best For | JVM Flag |
---|---|---|
G1GC (Default) | Balanced performance & latency | -XX:+UseG1GC |
ZGC | Low-latency applications | -XX:+UseZGC |
Shenandoah GC | Ultra-low pause times | -XX:+UseShenandoahGC |
Parallel GC | High-throughput batch processing | -XX:+UseParallelGC |
Serial GC | Low-memory environments (containers) | -XX:+UseSerialGC |
Adding these lines to Select our Algorithm
# NOTE WE CAN MERGE THESE 2 into singlar
ENV JAVA_OPTS="-XX:+UseG1GC"
CMD ["java", "$JAVA_OPTS", "-jar", "app.jar"]
ENV JAVA_CONTAINER="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75 -XX:InitialRAMPercentage=50 -XX:MinRAMPercentage=50"
CMD ["java", "$JAVA_OPTS","$JAVA_CONTAINER", "-jar", "app.jar"]
We also need an entrypoint:
# Assigning Permissions need to be done before jumping into normal user
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
RUN chown -R appuser:appgroup /app
You must have noticed by now that our builds are reaching 1 minute to build, This is because we are building it twice, once while creating the Jar and then executing. What if we can copy the layers from the first build?
think about 50 developers working on different builds! This reduces our time by half for simple application.
But there is a Catch?! Can anyone guess?
Yes for minor speed gain, we are adding manual files. and thats fine!
If you 💘 Optimization as these, we will have another session on:
Step #9: Use a Distroless Base Image
Step #10: Enable Class Data Sharing (CDS) for Faster Startup
Step #11: Remove Unused Layers with Squash (Reduces Size)
Step #12: Compress JAR Using UPX (Reduces Size)
Step #13: Use GraalVM Native Image for AOT Compilation
Step #14: Use docker scout to Remove Security Vulnerabilities
Step #15: Use Read-Only File System for Security
Step #16: Enable JVM Heap Dump on OOM (Better Debugging & Performance Tuning)
Thats All Folks!