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.