The first Docker image I built for a Spring Boot service was 780 MB: a full JDK, Maven, the source code and a fat JAR, all running as root. It worked, but every deploy pushed hundreds of megabytes and every scan found dozens of CVEs in tools the app never used. A multi-stage build fixes all of that.
The Dockerfile
# ---- 1. build ----
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /src
COPY pom.xml .
RUN mvn -B -q dependency:go-offline
COPY src ./src
RUN mvn -B -q package -DskipTests
# ---- 2. split the jar into layers ----
FROM eclipse-temurin:21-jre AS extract
WORKDIR /app
COPY --from=build /src/target/*.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --launcher --destination extracted
# ---- 3. runtime ----
FROM eclipse-temurin:21-jre
RUN useradd --system --uid 10001 appuser
WORKDIR /app
COPY --from=extract /app/extracted/dependencies/ ./
COPY --from=extract /app/extracted/spring-boot-loader/ ./
COPY --from=extract /app/extracted/snapshot-dependencies/ ./
COPY --from=extract /app/extracted/application/ ./
USER 10001
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Why each piece matters
- Build stage is thrown away. Maven, the JDK and your source never reach the final image.
dependency:go-offlinebeforeCOPY src. Dependencies get their own cached layer, so a code-only change does not re-download half of Maven Central.- Layered JAR. Spring Boot splits the app into dependencies, loader and application layers. Only the small application layer changes between releases, so pushes and pulls are fast. (The
-Djarmode=toolssyntax is Spring Boot 3.3+; older versions use-Djarmode=layertools -jar app.jar extract.) - JRE, not JDK. Smaller image and fewer packages for the scanner to flag. Distroless or Alpine-based JREs go smaller still if your team is comfortable debugging without a shell.
- Non-root user. Many Kubernetes clusters enforce
runAsNonRoot; set it in the image so it just works. MaxRAMPercentagesizes the heap from the container memory limit instead of a hard-coded-Xmx.
Don't forget .dockerignore
target/
.git/
.idea/
*.iml
**/*.log
Without it, COPY sends your whole working tree (including .git) to the build context.
Result
For a typical service the image dropped from about 780 MB to under 250 MB, rebuilds after a code change take seconds, and the image scan in CI went from dozens of findings to a handful that actually belong to the runtime. Run trivy image on both versions and compare for yourself.
No comments:
Post a Comment