Multi-stage Docker builds are a powerful feature that allows you to optimize your Java application Docker images by separating build dependencies from runtime dependencies. This article explores how to create efficient, secure, and production-ready Docker images for Java applications.
Basic Multi-Stage Docker Build
1. Simple Multi-Stage Maven Build
# Build stage FROM maven:3.8.5-openjdk-17 AS builder WORKDIR /app COPY pom.xml . COPY src ./src RUN mvn clean package -DskipTests # Runtime stage FROM openjdk:17-jre-slim WORKDIR /app # Copy the built JAR from builder stage COPY --from=builder /app/target/*.jar app.jar # Security: Run as non-root user RUN groupadd -r spring && useradd -r -g spring spring USER spring EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
2. Multi-Stage with Gradle
# Build stage with Gradle FROM gradle:7.4.2-jdk17 AS builder WORKDIR /app COPY build.gradle . COPY settings.gradle . COPY src ./src RUN gradle clean build -x test # Runtime stage FROM openjdk:17-jre-slim WORKDIR /app # Copy the built JAR COPY --from=builder /app/build/libs/*.jar app.jar # Create non-root user RUN addgroup --system --gid 1001 appgroup && \ adduser --system --uid 1001 --gid 1001 appuser USER appuser EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar"]
Advanced Multi-Stage Patterns
3. Multi-Stage with Dependency Caching
# Stage 1: Dependency resolution FROM maven:3.8.5-openjdk-17 AS deps WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline # Stage 2: Build application FROM maven:3.8.5-openjdk-17 AS builder WORKDIR /app COPY --from=deps /root/.m2 /root/.m2 COPY pom.xml . COPY src ./src RUN mvn clean package -DskipTests # Stage 3: Runtime environment FROM openjdk:17-jre-slim AS runtime WORKDIR /app # Install security updates RUN apt-get update && \ apt-get upgrade -y && \ rm -rf /var/lib/apt/lists/* # Copy application COPY --from=builder /app/target/*.jar app.jar # Security hardening RUN groupadd -r spring && useradd -r -g spring spring && \ chown -R spring:spring /app USER spring # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
4. Multi-Architecture Build
# Build stage for multiple architectures FROM --platform=$BUILDPLATFORM maven:3.8.5-openjdk-17 AS builder WORKDIR /app COPY pom.xml . COPY src ./src RUN mvn clean package -DskipTests # Runtime stage FROM openjdk:17-jre-slim WORKDIR /app # Install necessary packages for your application RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/target/*.jar app.jar # Create non-root user RUN addgroup --system --gid 1001 appgroup && \ adduser --system --uid 1001 --gid 1001 appuser USER appuser EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
Spring Boot Specific Optimizations
5. Spring Boot with Layered Jars
# Build stage FROM maven:3.8.5-openjdk-17 AS builder WORKDIR /app COPY pom.xml . COPY src ./src RUN mvn clean package -DskipTests # Extract layers FROM builder AS extractor WORKDIR /app RUN java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted # Runtime stage FROM openjdk:17-jre-slim WORKDIR /app # Copy layers from extractor COPY --from=extractor /app/target/extracted/dependencies/ ./ COPY --from=extractor /app/target/extracted/spring-boot-loader/ ./ COPY --from=extractor /app/target/extracted/snapshot-dependencies/ ./ COPY --from=extractor /app/target/extracted/application/ ./ # Create non-root user RUN groupadd -r spring && useradd -r -g spring spring USER spring EXPOSE 8080 ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
6. Spring Boot with JVM Optimization
# Build stage FROM maven:3.8.5-openjdk-17 AS builder WORKDIR /app COPY pom.xml . COPY src ./src RUN mvn clean package -DskipTests # Runtime stage with JVM optimizations FROM openjdk:17-jre-slim WORKDIR /app COPY --from=builder /app/target/*.jar app.jar # JVM optimization flags ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions" # Create non-root user RUN groupadd -r spring && useradd -r -g spring spring USER spring EXPOSE 8080 ENTRYPOINT exec java $JAVA_OPTS -jar app.jar
Security Hardened Builds
7. Security-Focused Multi-Stage Build
# Build stage FROM maven:3.8.5-openjdk-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # Security scan stage (optional) FROM aquasec/trivy:latest AS security-scan WORKDIR /app COPY --from=builder /app/target/*.jar . RUN trivy filesystem --exit-code 1 --no-progress / # Runtime stage with security hardening FROM openjdk:17-jre-slim WORKDIR /app # Security updates and cleanup RUN apt-get update && \ apt-get upgrade -y && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # Copy application COPY --from=builder /app/target/*.jar app.jar # Security: Non-root user with specific UID/GID RUN groupadd -g 1000 appuser && \ useradd -r -u 1000 -g appuser appuser && \ chown -R appuser:appuser /app USER 1000 # Security: Read-only filesystem RUN chmod -R go-w /app EXPOSE 8080 # Use exec form for better signal handling ENTRYPOINT ["java", "-jar", "app.jar"]
8. Distroless Java Image
# Build stage FROM maven:3.8.5-openjdk-17 AS builder WORKDIR /app COPY pom.xml . COPY src ./src RUN mvn clean package -DskipTests # Runtime stage with Google Distroless FROM gcr.io/distroless/java17:nonroot WORKDIR /app COPY --from=builder /app/target/*.jar app.jar # Distroless images run as non-root by default EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
Advanced Build Scenarios
9. Multi-Module Maven Project
# Stage 1: Build all modules FROM maven:3.8.5-openjdk-17 AS builder WORKDIR /app # Copy parent POM and all module POMs COPY pom.xml . COPY api/pom.xml api/pom.xml COPY service/pom.xml service/pom.xml COPY web/pom.xml web/pom.xml # Download dependencies RUN mvn dependency:go-offline # Copy source code COPY api/src api/src COPY service/src service/src COPY web/src web/src # Build all modules RUN mvn clean package -DskipTests # Stage 2: Runtime for web module FROM openjdk:17-jre-slim WORKDIR /app COPY --from=builder /app/web/target/*.jar app.jar RUN groupadd -r app && useradd -r -g app app USER app EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
10. Custom JRE with jlink
# Build stage with JDK
FROM openjdk:17-jdk-slim AS builder
WORKDIR /app
COPY . .
RUN ./mvnw clean package -DskipTests
# Create custom JRE stage
FROM openjdk:17-jdk-slim AS jre-builder
RUN jlink \
--add-modules java.base,java.logging,java.management,java.naming,java.sql,java.desktop,java.security.jgss \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /custom-jre
# Final stage with custom JRE
FROM debian:bullseye-slim
WORKDIR /app
# Copy custom JRE
COPY --from=jre-builder /custom-jre /usr/local/java
# Set Java environment
ENV JAVA_HOME=/usr/local/java
ENV PATH="${JAVA_HOME}/bin:${PATH}"
# Copy application
COPY --from=builder /app/target/*.jar app.jar
# Create non-root user
RUN groupadd -r app && useradd -r -g app app
USER app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Environment-Specific Builds
11. Profile-Based Build Configuration
# Build stage with profile support
FROM maven:3.8.5-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
ARG BUILD_PROFILE=prod
RUN mvn clean package -DskipTests -P${BUILD_PROFILE}
# Runtime stage
FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
# Set environment based on build profile
ARG BUILD_PROFILE=prod
ENV SPRING_PROFILES_ACTIVE=${BUILD_PROFILE}
RUN groupadd -r app && useradd -r -g app app
USER app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
12. Development vs Production Builds
Dockerfile.dev:
# Development build with hot reload FROM maven:3.8.5-openjdk-17 WORKDIR /app COPY . . # Install DevTools for hot reload RUN mvn clean compile EXPOSE 8080 CMD ["mvn", "spring-boot:run"]
Dockerfile.prod:
# Production multi-stage build FROM maven:3.8.5-openjdk-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:17-jre-slim WORKDIR /app COPY --from=builder /app/target/*.jar app.jar RUN groupadd -r app && useradd -r -g app app USER app EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
Build Optimization Techniques
13. Docker BuildKit Optimization
# syntax=docker/dockerfile:1.4 # Build stage with cache mounts FROM maven:3.8.5-openjdk-17 AS builder WORKDIR /app # Cache Maven dependencies COPY pom.xml . RUN --mount=type=cache,target=/root/.m2 mvn dependency:go-offline # Build application COPY src ./src RUN --mount=type=cache,target=/root/.m2 mvn clean package -DskipTests # Runtime stage FROM openjdk:17-jre-slim WORKDIR /app COPY --from=builder /app/target/*.jar app.jar RUN groupadd -r app && useradd -r -g app app USER app EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
14. Multi-Stage with Testing
# Test stage FROM maven:3.8.5-openjdk-17 AS tester WORKDIR /app COPY pom.xml . COPY src ./src RUN mvn test # Build stage FROM maven:3.8.5-openjdk-17 AS builder WORKDIR /app COPY --from=tester /app/target/surefire-reports /app/target/surefire-reports COPY pom.xml . COPY src ./src RUN mvn clean package -DskipTests # Runtime stage FROM openjdk:17-jre-slim WORKDIR /app COPY --from=builder /app/target/*.jar app.jar RUN groupadd -r app && useradd -r -g app app USER app EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
Docker Compose Integration
15. docker-compose.yml for Multi-Stage Builds
version: '3.8' services: app: build: context: . dockerfile: Dockerfile target: builder # You can specify which stage to build ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=docker depends_on: - postgres networks: - app-network postgres: image: postgres:13 environment: POSTGRES_DB: myapp POSTGRES_USER: appuser POSTGRES_PASSWORD: apppass volumes: - postgres_data:/var/lib/postgresql/data networks: - app-network volumes: postgres_data: networks: app-network: driver: bridge
Build Scripts and CI/CD Integration
16. Build Script Examples
build.sh:
#!/bin/bash
# Build with different profiles
build_image() {
local profile=$1
local tag=$2
docker build \
--build-arg BUILD_PROFILE=$profile \
-t myapp:$tag \
.
}
# Build development image
build_image "dev" "latest-dev"
# Build production image
build_image "prod" "latest"
# Security scan
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest \
image myapp:latest
Jenkinsfile:
pipeline {
agent any
stages {
stage('Build') {
steps {
script {
docker.build("myapp:${env.BUILD_ID}", "--target builder .")
}
}
}
stage('Test') {
steps {
sh 'docker run myapp:${env.BUILD_ID} mvn test'
}
}
stage('Security Scan') {
steps {
sh 'docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image myapp:${env.BUILD_ID}'
}
}
stage('Production Build') {
steps {
script {
docker.build("myapp:prod-${env.BUILD_ID}", ".")
}
}
}
}
}
Best Practices Summary
Key Benefits of Multi-Stage Builds:
- Smaller Image Sizes: Eliminate build dependencies from final image
- Improved Security: Fewer packages and non-root users
- Better Caching: Separate dependency and source code layers
- Reproducible Builds: Consistent build environment
- CI/CD Optimization: Faster builds with layer caching
Essential Security Practices:
- Use minimal base images (slim, alpine, distroless)
- Run as non-root user
- Regularly update base images
- Scan for vulnerabilities
- Use trusted base images
Performance Optimizations:
- Leverage Docker BuildKit
- Use cache mounts for package managers
- Separate dependency resolution from build
- Use .dockerignore to exclude unnecessary files
By implementing these multi-stage Docker build patterns, you can create optimized, secure, and efficient Java application containers suitable for production deployment.