DEV Community

Nitish
Nitish

Posted on

Spring Boot 3.2 + Docker Multi-Stage Builds: Slim Production Images for Microservices

Spring Boot 3.2 + Docker Multi-Stage Builds: Slim Production Images for Microservices

Without optimized Docker builds, your Spring Boot microservices deploy 650MB images containing build tools and debug symbols. Production containers run with unnecessary vulnerabilities and waste storage/bandwidth during scaling events.

Prerequisites

  • Java 17 SDK (Eclipse Temurin 17.0.9)
  • Spring Boot 3.2.4
  • Docker Engine 24.0.6+
  • Maven 3.9.6
  • Basic understanding of Dockerfile syntax

Building Fat JARs the Traditional Way

Spring Boot's Maven plugin packages dependencies and application code into a single executable JAR. A naive Dockerfile copies this JAR into a full JDK image:

FROM eclipse-temurin:17-jdk
COPY target/myapp.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Enter fullscreen mode Exit fullscreen mode

This creates a 567MB image containing the full JDK, Maven dependencies, and build artifacts - none of which are needed at runtime.

Multi-Stage Build Optimization

Split the Dockerfile into build and runtime phases. The first stage compiles the application, while the final stage copies only necessary artifacts:

# Build stage
FROM eclipse-temurin:17-jdk as builder
WORKDIR /app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY src/ src/
RUN ./mvnw package -DskipTests

# Runtime stage  
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar /app.jar
RUN adduser --system --no-create-home appuser
USER appuser
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
Enter fullscreen mode Exit fullscreen mode

This reduces image size by 68% (180MB final image) and removes build tools from production. The Alpine-based JRE provides a minimal Linux environment.

Image Size Reduction Techniques

Further optimize by stripping unused dependencies and compressing layers:

# In build stage:
RUN ./mvnw package -DskipTests -Dspring-boot.repackage.excludeDevtools=true

# In runtime stage:
RUN apk add --no-cache libstdc++ # Only if native dependencies exist
Enter fullscreen mode Exit fullscreen mode

Common Mistakes

Mistake 1: Not pruning Maven dependencies in build stage

# Wrong: Copies entire project before resolving dependencies
COPY . .
RUN mvn package
Enter fullscreen mode Exit fullscreen mode
# Fixed: Layer caching for dependencies
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src/ src/
RUN mvn package
Enter fullscreen mode Exit fullscreen mode

Copying the POM first allows Docker to cache dependency downloads separately from source code changes.

Mistake 2: Running as root in production

# Wrong: Defaults to root user
ENTRYPOINT ["java","-jar","/app.jar"]
Enter fullscreen mode Exit fullscreen mode
# Fixed: Non-privileged user
RUN adduser --system --no-create-home appuser
USER appuser
ENTRYPOINT ["java","-jar","/app.jar"]
Enter fullscreen mode Exit fullscreen mode

Root containers pose security risks. Alpine's adduser creates a minimal user account without a home directory.

Mistake 3: Using JDK instead of JRE in runtime

# Wrong: Includes compiler tools
FROM eclipse-temurin:17-jdk
Enter fullscreen mode Exit fullscreen mode
# Correct: Minimal JRE
FROM eclipse-temurin:17-jre-alpine
Enter fullscreen mode Exit fullscreen mode

The JDK adds 140MB of development tools unused in production. Alpine's musl libc further reduces size over glibc.

Summary

  • Use separate build and runtime stages to exclude development dependencies
  • Prefer JRE-alpine base images for production deployments
  • Create non-root users and enable security hardening flags
  • Layer Dockerfile commands to optimize build caching
  • Strip debug symbols and exclude devtools from production JARs

The author publishes Spring Boot starter templates at https://gumroad.com

Top comments (0)