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"]
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"]
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
Common Mistakes
Mistake 1: Not pruning Maven dependencies in build stage
# Wrong: Copies entire project before resolving dependencies
COPY . .
RUN mvn package
# Fixed: Layer caching for dependencies
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src/ src/
RUN mvn package
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"]
# Fixed: Non-privileged user
RUN adduser --system --no-create-home appuser
USER appuser
ENTRYPOINT ["java","-jar","/app.jar"]
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
# Correct: Minimal JRE
FROM eclipse-temurin:17-jre-alpine
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)