Back to Blog
spring-bootdockerkubernetesdevopsjavagraalvm

Spring Boot Docker Image Optimization: From 800MB to Under 150MB (2026)

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.

J

JOptimize Team

May 23, 2026· 8 min read

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.


The Problem: Naive Dockerfile

# 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 base
  • Entire fat JAR is one layer - every code change rebuilds the full layer
  • Full JDK included - contains compiler, tools you don't need at runtime
  • Runs as root - security risk

Result: ~650-900MB image, full rebuild on every change.


Step 1: Use Layered JARs

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 --from=builder /app/dependencies/ ./ COPY --from=builder /app/spring-boot-loader/ ./ COPY --from=builder /app/snapshot-dependencies/ ./ COPY --from=builder /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.


Step 2: Switch to Distroless or Alpine

# 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 --from=builder /app/dependencies/ ./ COPY --from=builder /app/spring-boot-loader/ ./ COPY --from=builder /app/snapshot-dependencies/ ./ COPY --from=builder /app/application/ ./ ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Step 3: JVM Tuning for Containers

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 workloads

Step 4: GraalVM Native Image (Maximum Reduction)

For 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 --from=native-builder /app/target/app /app EXPOSE 8080 ENTRYPOINT ["/app"]

Comparison:

ApproachImage SizeStartup TimeMemory
Naive (openjdk:17)~850MB5-8s350MB
Layered + JRE Alpine~190MB3-5s300MB
Distroless + Layered~145MB3-5s290MB
GraalVM Native~85MB0.1-0.3s60MB

Spring Boot Buildpacks (Alternative to Dockerfile)

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.


Common Mistakes to Avoid

  • Using openjdk Docker Hub images - these are deprecated; use eclipse-temurin (Adoptium) instead
  • Copying the fat JAR as a single layer - every code change invalidates the entire layer cache; always use layered extraction
  • Running as root inside the container - creates a security risk; always add a non-root user
  • Not setting container memory flags - without -XX:+UseContainerSupport, the JVM uses host memory and gets OOM-killed

Summary

Shrinking 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.


Optimize Your Spring Boot Deployment Pipeline

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.

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.