Multi-Stage Docker Builds for Java Applications

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:

  1. Smaller Image Sizes: Eliminate build dependencies from final image
  2. Improved Security: Fewer packages and non-root users
  3. Better Caching: Separate dependency and source code layers
  4. Reproducible Builds: Consistent build environment
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper