A fat Spring Boot Docker image wastes bandwidth, slows deployments, and increases attack surface. Learn layered builds, distroless images, and GraalVM native to shrink your images.
JOptimize Team
A default Spring Boot Docker image built with a full JDK base easily reaches 700-900MB. Every deploy pushes this over the network, Kubernetes pulls it for every pod, and your CI/CD pipeline stores multiple versions. Optimizing the image doesn't just save storage - it cuts deploy times, reduces cold start latency, and shrinks your attack surface.
# What most tutorials show - DO NOT use in production FROM openjdk:17 COPY target/app.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"]
Problems:
openjdk:17 is based on Debian - ~470MB just for the baseResult: ~650-900MB image, full rebuild on every change.
Spring Boot 2.3+ supports layered JARs - the fat JAR is split into layers ordered by how frequently they change:
1. dependencies - rarely changes (external libs) 2. spring-boot-loader - rarely changes 3. snapshot-dependencies - changes occasionally 4. application - changes on every commit
# Stage 1: Extract layers FROM eclipse-temurin:21-jre-alpine AS builder WORKDIR /app COPY target/app.jar app.jar RUN java -Djarmode=layertools -jar app.jar extract # Stage 2: Final image - only JRE, not full JDK FROM eclipse-temurin:21-jre-alpine WORKDIR /app # Add non-root user RUN addgroup -S spring && adduser -S spring -G spring USER spring # Copy layers in order of change frequency COPY /app/dependencies/ ./ COPY /app/spring-boot-loader/ ./ COPY /app/snapshot-dependencies/ ./ COPY /app/application/ ./ EXPOSE 8080 ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Result: ~200MB image. More importantly, only the application layer rebuilds on code changes - CI/CD layer cache hits on dependencies save minutes per build.
# Option A: eclipse-temurin:21-jre-alpine (~175MB) FROM eclipse-temurin:21-jre-alpine # Option B: Google Distroless (~130MB) - no shell, minimal attack surface FROM gcr.io/distroless/java21-debian12 # Option C: eclipse-temurin:21-jre-jammy (~220MB) - if you need apt FROM eclipse-temurin:21-jre-jammy
Distroless images contain only the JRE and your app - no shell, no package manager, no utilities. This eliminates entire categories of container vulnerabilities.
# Distroless example FROM eclipse-temurin:21-jre-alpine AS builder WORKDIR /app COPY target/app.jar app.jar RUN java -Djarmode=layertools -jar app.jar extract FROM gcr.io/distroless/java21-debian12 WORKDIR /app COPY /app/dependencies/ ./ COPY /app/spring-boot-loader/ ./ COPY /app/snapshot-dependencies/ ./ COPY /app/application/ ./ ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
ENTRYPOINT ["java", \ "-XX:+UseContainerSupport", \ "-XX:MaxRAMPercentage=75.0", \ "-XX:+UseZGC", "-XX:+ZGenerational", \ "-Dspring.aot.enabled=true", \ "org.springframework.boot.loader.launch.JarLauncher"]
-XX:+UseContainerSupport - reads container memory limits, not host memory-XX:MaxRAMPercentage=75.0 - uses 75% of container memory for heap-XX:+UseZGC -XX:+ZGenerational - best GC for containerized low-latency workloadsFor serverless or scale-to-zero Kubernetes workloads:
# Multi-stage GraalVM native build FROM ghcr.io/graalvm/native-image-community:21 AS native-builder WORKDIR /app # Install Maven COPY .mvn/ .mvn/ COPY mvnw pom.xml ./ RUN ./mvnw dependency:go-offline COPY src/ src/ RUN ./mvnw -Pnative native:compile -DskipTests # Tiny final image - no JVM at all! FROM gcr.io/distroless/base-debian12 COPY /app/target/app /app EXPOSE 8080 ENTRYPOINT ["/app"]
Comparison:
| Approach | Image Size | Startup Time | Memory |
|---|---|---|---|
| Naive (openjdk:17) | ~850MB | 5-8s | 350MB |
| Layered + JRE Alpine | ~190MB | 3-5s | 300MB |
| Distroless + Layered | ~145MB | 3-5s | 290MB |
| GraalVM Native | ~85MB | 0.1-0.3s | 60MB |
Spring Boot 3.x integrates with Paketo Buildpacks for zero-Dockerfile image builds:
mvn spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:latest
<!-- pom.xml - configure the buildpack --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <image> <name>myapp:${project.version}</name> <env> <BP_JVM_VERSION>21</BP_JVM_VERSION> <BPE_JAVA_TOOL_OPTIONS>-XX:MaxRAMPercentage=75</BPE_JAVA_TOOL_OPTIONS> </env> </image> </configuration> </plugin>
Buildpacks automatically apply layering, use a minimal JRE, and include CDS for faster startup.
openjdk Docker Hub images - these are deprecated; use eclipse-temurin (Adoptium) instead-XX:+UseContainerSupport, the JVM uses host memory and gets OOM-killedShrinking a Spring Boot Docker image goes through three stages: layered JAR extraction (cuts build times), Alpine/distroless base images (cuts image size to ~150MB), and GraalVM native (cuts to ~80MB with sub-second startup). Most apps get most of the benefit from just the first two steps - no GraalVM complexity required.
JOptimize analyzes your Spring Boot project for deployment anti-patterns - missing container memory flags, un-tuned thread pools for containerized environments, and configuration that works on a laptop but fails in a container.
Make your Spring Boot app container-ready - free scan, no configuration required.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.