Multi-stage builds optimize Docker images by separating build-time dependencies from runtime dependencies, resulting in smaller, more secure, and faster images.
Core Concepts
What are Multi-Stage Builds?
- Multiple
FROMstatements in a single Dockerfile - Each stage can have different base images and dependencies
- Copy artifacts from one stage to another
- Final image contains only runtime dependencies
Benefits:
- Smaller Images: Exclude build tools and dependencies
- Improved Security: Fewer attack surfaces
- Better Caching: Independent layer caching per stage
- Simplified Build Process: Single Dockerfile for build and runtime
Dependencies and Setup
Project Structure
project/ âââ src/ â âââ main/ â âââ java/ â âââ com/ â âââ example/ â âââ Application.java âââ Dockerfile âââ Dockerfile.multistage âââ docker-compose.yml âââ pom.xml âââ README.md
Maven Dependencies
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven-surefire-plugin.version>3.0.0</maven-surefire-plugin.version>
<spring-boot.version>3.1.0</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.example.Application</mainClass>
<finalName>${project.artifactId}</finalName>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
</plugins>
</build>
Dockerfile Implementations
1. Basic Multi-Stage Build
# Dockerfile.multistage # Stage 1: Build stage FROM maven:3.9.4-eclipse-temurin-17 AS builder # Set working directory WORKDIR /app # Copy pom.xml and download dependencies COPY pom.xml . RUN mvn dependency:go-offline -B # Copy source code COPY src ./src # Build the application RUN mvn clean package -DskipTests # Stage 2: Runtime stage FROM eclipse-temurin:17-jre-jammy # Install security updates RUN apt-get update && \ apt-get upgrade -y && \ rm -rf /var/lib/apt/lists/* # Create non-root user RUN groupadd -r spring && useradd -r -g spring spring USER spring # Set working directory WORKDIR /app # Copy JAR from build stage COPY --from=builder --chown=spring:spring /app/target/*.jar app.jar # Expose port EXPOSE 8080 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 # Run the application ENTRYPOINT ["java", "-jar", "/app/app.jar"]
2. Advanced Multi-Stage with Optimization
# Dockerfile.optimized # Stage 1: Dependency resolution FROM maven:3.9.4-eclipse-temurin-17 AS deps WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B # Stage 2: Test stage (optional) FROM deps AS tester COPY src ./src RUN mvn test -B # Stage 3: Builder stage FROM deps AS builder COPY src ./src RUN mvn clean package -DskipTests # Extract layers for better caching RUN java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted # Stage 4: Runtime stage FROM eclipse-temurin:17-jre-jammy AS runtime # Security hardening RUN apt-get update && \ apt-get upgrade -y && \ apt-get install -y --no-install-recommends curl && \ rm -rf /var/lib/apt/lists/* && \ apt-get clean # Create application user RUN groupadd -r spring && useradd --no-log-init -r -g spring spring # Create app directory RUN mkdir -p /app && chown spring:spring /app WORKDIR /app USER spring # Copy layers from builder stage COPY --from=builder --chown=spring:spring /app/target/extracted/dependencies/ ./ COPY --from=builder --chown=spring:spring /app/target/extracted/spring-boot-loader/ ./ COPY --from=builder --chown=spring:spring /app/target/extracted/snapshot-dependencies/ ./ COPY --from=builder --chown=spring:spring /app/target/extracted/application/ ./ # JVM optimization ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:MaxGCPauseMillis=200" EXPOSE 8080 ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
3. Distroless-Based Multi-Stage
# Dockerfile.distroless # Stage 1: Build stage FROM maven:3.9.4-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests # Stage 2: Runtime with Distroless FROM gcr.io/distroless/java17-debian11:nonroot # Copy application COPY --from=builder /app/target/*.jar /app/app.jar # Use non-root user (provided by distroless) USER nonroot EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar"]
4. Alpine Linux Multi-Stage
# Dockerfile.alpine # Stage 1: Build stage FROM maven:3.9.4-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests # Stage 2: Alpine runtime FROM eclipse-temurin:17-jre-alpine # Install minimal packages RUN apk update && \ apk upgrade && \ apk add --no-cache curl && \ rm -rf /var/cache/apk/* # Create non-root user RUN addgroup -S spring && adduser -S spring -G spring USER spring WORKDIR /app COPY --from=builder --chown=spring:spring /app/target/*.jar app.jar EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 ENTRYPOINT ["java", "-jar", "/app/app.jar"]
5. GraalVM Native Image Multi-Stage
# Dockerfile.native # Stage 1: Build stage FROM maven:3.9.4-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests # Stage 2: GraalVM native image build FROM ghcr.io/graalvm/native-image:ol8-java17-22 AS native-builder WORKDIR /app COPY --from=builder /app/target/*.jar app.jar # Build native image RUN native-image -jar app.jar --no-fallback -H:Name=application # Stage 3: Minimal runtime FROM oraclelinux:8-slim # Install security updates RUN microdnf update -y && \ microdnf install -y gcompat && \ microdnf clean all # Create non-root user RUN groupadd -r spring && useradd -r -g spring spring USER spring WORKDIR /app COPY --from=native-builder --chown=spring:spring /app/application . EXPOSE 8080 ENTRYPOINT ["./application"]
Java Build Optimization Classes
1. Docker Build Service
@Service
@Slf4j
public class DockerBuildService {
private final DockerClient dockerClient;
private final BuildOptimizationService optimizationService;
public DockerBuildService(DockerClient dockerClient,
BuildOptimizationService optimizationService) {
this.dockerClient = dockerClient;
this.optimizationService = optimizationService;
}
public BuildResult buildMultiStageImage(BuildRequest request) {
log.info("Building multi-stage Docker image: {}", request.getImageName());
try {
// Analyze project for optimization opportunities
ProjectAnalysis analysis = optimizationService.analyzeProject(request.getProjectPath());
// Select appropriate Dockerfile template
String dockerfileContent = selectDockerfileTemplate(request, analysis);
// Build image
String imageId = buildImage(request, dockerfileContent);
// Analyze built image
ImageAnalysis imageAnalysis = analyzeBuiltImage(imageId);
return BuildResult.success(imageId, imageAnalysis);
} catch (Exception e) {
log.error("Docker build failed", e);
return BuildResult.failure("Build failed: " + e.getMessage());
}
}
public BuildResult buildOptimizedImage(OptimizedBuildRequest request) {
log.info("Building optimized image with strategy: {}", request.getOptimizationStrategy());
try {
// Generate optimized Dockerfile
String dockerfileContent = optimizationService.generateOptimizedDockerfile(request);
// Build with cache optimization
BuildCommand buildCommand = BuildCommand.builder()
.dockerfileContent(dockerfileContent)
.contextPath(request.getProjectPath())
.imageName(request.getImageName())
.cacheFrom(request.getCacheFrom())
.buildArgs(request.getBuildArgs())
.build();
String imageId = executeBuildWithCache(buildCommand);
// Perform post-build optimization
optimizationService.optimizeImageLayers(imageId);
return BuildResult.success(imageId, analyzeBuiltImage(imageId));
} catch (Exception e) {
log.error("Optimized build failed", e);
return BuildResult.failure("Optimized build failed: " + e.getMessage());
}
}
public ImageComparison compareBuildStrategies(String projectPath) {
List<BuildStrategyResult> results = new ArrayList<>();
// Test different build strategies
BuildStrategy[] strategies = BuildStrategy.values();
for (BuildStrategy strategy : strategies) {
BuildResult result = testBuildStrategy(projectPath, strategy);
results.add(new BuildStrategyResult(strategy, result));
}
return new ImageComparison(results);
}
private String selectDockerfileTemplate(BuildRequest request, ProjectAnalysis analysis) {
if (analysis.isSpringBoot()) {
return generateSpringBootDockerfile(request, analysis);
} else if (analysis.isNativeCompatible()) {
return generateNativeImageDockerfile(request, analysis);
} else {
return generateStandardJavaDockerfile(request, analysis);
}
}
private String generateSpringBootDockerfile(BuildRequest request, ProjectAnalysis analysis) {
return """
# Multi-stage build for Spring Boot application
FROM maven:3.9.4-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre-jammy
RUN groupadd -r spring && useradd -r -g spring spring
USER spring
WORKDIR /app
COPY --from=builder --chown=spring:spring /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
""";
}
// Additional implementation details would go here...
}
2. Build Optimization Service
@Service
@Slf4j
public class BuildOptimizationService {
private final ProjectAnalyzer projectAnalyzer;
private final DockerfileGenerator dockerfileGenerator;
public ProjectAnalysis analyzeProject(String projectPath) {
log.info("Analyzing project at: {}", projectPath);
ProjectAnalysis analysis = new ProjectAnalysis();
try {
// Analyze project type
analysis.setProjectType(analyzeProjectType(projectPath));
// Check dependencies
analysis.setDependencies(analyzeDependencies(projectPath));
// Calculate size metrics
analysis.setSizeMetrics(calculateSizeMetrics(projectPath));
// Check for optimization opportunities
analysis.setOptimizations(findOptimizations(analysis));
return analysis;
} catch (Exception e) {
log.error("Project analysis failed", e);
throw new BuildOptimizationException("Project analysis failed", e);
}
}
public String generateOptimizedDockerfile(OptimizedBuildRequest request) {
ProjectAnalysis analysis = analyzeProject(request.getProjectPath());
DockerfileTemplate template = DockerfileTemplate.builder()
.baseImage(selectBaseImage(analysis, request.getOptimizationStrategy()))
.buildStages(generateBuildStages(analysis))
.runtimeConfig(generateRuntimeConfig(analysis))
.optimizations(generateOptimizations(analysis))
.build();
return dockerfileGenerator.generate(template);
}
public void optimizeImageLayers(String imageId) {
try {
// Remove unnecessary files
removeBuildArtifacts(imageId);
// Clean package caches
cleanPackageCaches(imageId);
// Optimize file permissions
optimizePermissions(imageId);
} catch (Exception e) {
log.warn("Image optimization failed: {}", e.getMessage());
}
}
public BuildReport generateBuildReport(BuildResult result) {
return BuildReport.builder()
.imageId(result.getImageId())
.sizeAnalysis(analyzeImageSize(result.getImageId()))
.layerAnalysis(analyzeImageLayers(result.getImageId()))
.securityAnalysis(analyzeSecurity(result.getImageId()))
.optimizationSuggestions(generateSuggestions(result))
.build();
}
private ProjectType analyzeProjectType(String projectPath) {
File pomFile = new File(projectPath, "pom.xml");
File gradleFile = new File(projectPath, "build.gradle");
if (pomFile.exists()) {
return analyzeMavenProject(pomFile);
} else if (gradleFile.exists()) {
return ProjectType.GRADLE;
} else {
return ProjectType.UNKNOWN;
}
}
private ProjectType analyzeMavenProject(File pomFile) {
try {
MavenXpp3Reader reader = new MavenXpp3Reader();
Model model = reader.read(new FileReader(pomFile));
// Check for Spring Boot
if (model.getParent() != null &&
model.getParent().getArtifactId().contains("spring-boot")) {
return ProjectType.SPRING_BOOT;
}
// Check for Quarkus
if (hasDependency(model, "io.quarkus")) {
return ProjectType.QUARKUS;
}
// Check for Micronaut
if (hasDependency(model, "io.micronaut")) {
return ProjectType.MICRONAUT;
}
return ProjectType.STANDARD_MAVEN;
} catch (Exception e) {
log.warn("Failed to analyze Maven project", e);
return ProjectType.STANDARD_MAVEN;
}
}
private boolean hasDependency(Model model, String groupId) {
return model.getDependencies().stream()
.anyMatch(dep -> dep.getGroupId().startsWith(groupId));
}
// Additional analysis methods...
}
3. Project Analyzer
@Component
@Slf4j
public class ProjectAnalyzer {
public DependencyAnalysis analyzeDependencies(String projectPath) {
DependencyAnalysis analysis = new DependencyAnalysis();
try {
File pomFile = new File(projectPath, "pom.xml");
if (pomFile.exists()) {
return analyzeMavenDependencies(pomFile);
}
// Add Gradle support here
} catch (Exception e) {
log.error("Dependency analysis failed", e);
}
return analysis;
}
public SizeMetrics calculateSizeMetrics(String projectPath) {
SizeMetrics metrics = new SizeMetrics();
try {
// Calculate source code size
long sourceSize = calculateDirectorySize(new File(projectPath, "src"));
metrics.setSourceSize(sourceSize);
// Calculate dependency size
long dependencySize = calculateDependencySize(projectPath);
metrics.setDependencySize(dependencySize);
// Calculate build output size
long buildOutputSize = calculateBuildOutputSize(projectPath);
metrics.setBuildOutputSize(buildOutputSize);
} catch (Exception e) {
log.warn("Size calculation failed", e);
}
return metrics;
}
public List<Optimization> findOptimizations(ProjectAnalysis analysis) {
List<Optimization> optimizations = new ArrayList<>();
// Check for large dependencies that could be optimized
if (analysis.getDependencySize() > 100 * 1024 * 1024) { // 100MB
optimizations.add(Optimization.DEPENDENCY_OPTIMIZATION);
}
// Check if application can use smaller base image
if (analysis.isCompatibleWithDistroless()) {
optimizations.add(Optimization.DISTROLESS_BASE);
}
// Check for native image compatibility
if (analysis.isNativeCompatible()) {
optimizations.add(Optimization.NATIVE_IMAGE);
}
// Check for layer optimization opportunities
if (analysis.hasLayerSplittingOpportunities()) {
optimizations.add(Optimization.LAYER_OPTIMIZATION);
}
return optimizations;
}
private DependencyAnalysis analyzeMavenDependencies(File pomFile) throws Exception {
DependencyAnalysis analysis = new DependencyAnalysis();
MavenXpp3Reader reader = new MavenXpp3Reader();
Model model = reader.read(new FileReader(pomFile));
// Analyze dependencies
List<DependencyInfo> dependencies = model.getDependencies().stream()
.map(this::mapDependency)
.collect(Collectors.toList());
analysis.setDependencies(dependencies);
analysis.setTotalDependencies(dependencies.size());
// Calculate approximate size (this would require more sophisticated analysis)
long estimatedSize = dependencies.stream()
.mapToLong(DependencyInfo::getEstimatedSize)
.sum();
analysis.setEstimatedSize(estimatedSize);
return analysis;
}
private DependencyInfo mapDependency(org.apache.maven.model.Dependency mavenDep) {
return DependencyInfo.builder()
.groupId(mavenDep.getGroupId())
.artifactId(mavenDep.getArtifactId())
.version(mavenDep.getVersion())
.scope(mavenDep.getScope() != null ? mavenDep.getScope() : "compile")
.build();
}
private long calculateDirectorySize(File directory) {
if (!directory.exists()) return 0;
return Arrays.stream(directory.listFiles())
.mapToLong(file -> {
if (file.isDirectory()) {
return calculateDirectorySize(file);
} else {
return file.length();
}
})
.sum();
}
}
4. Dockerfile Generator
@Component
public class DockerfileGenerator {
public String generate(DockerfileTemplate template) {
StringBuilder dockerfile = new StringBuilder();
// Generate build stages
for (BuildStage stage : template.getBuildStages()) {
dockerfile.append(generateStage(stage)).append("\n\n");
}
// Generate runtime stage
dockerfile.append(generateRuntimeStage(template.getRuntimeConfig()));
return dockerfile.toString();
}
public String generateForStrategy(ProjectAnalysis analysis, BuildStrategy strategy) {
switch (strategy) {
case STANDARD_MULTI_STAGE:
return generateStandardMultiStage(analysis);
case DISTROLESS:
return generateDistroless(analysis);
case ALPINE:
return generateAlpine(analysis);
case NATIVE:
return generateNative(analysis);
case LAYER_OPTIMIZED:
return generateLayerOptimized(analysis);
default:
return generateStandardMultiStage(analysis);
}
}
private String generateStandardMultiStage(ProjectAnalysis analysis) {
return """
# Standard multi-stage build
FROM maven:3.9.4-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre-jammy
RUN groupadd -r spring && useradd -r -g spring spring
USER spring
WORKDIR /app
COPY --from=builder --chown=spring:spring /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
""";
}
private String generateDistroless(ProjectAnalysis analysis) {
return """
# Distroless multi-stage build
FROM maven:3.9.4-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
FROM gcr.io/distroless/java17-debian11:nonroot
COPY --from=builder /app/target/*.jar /app/app.jar
USER nonroot
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
""";
}
private String generateStage(BuildStage stage) {
return String.format("""
# %s
FROM %s AS %s
WORKDIR %s
%s
""",
stage.getDescription(),
stage.getBaseImage(),
stage.getName(),
stage.getWorkDir(),
String.join("\n", stage.getCommands())
);
}
}
Models and Data Transfer Objects
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BuildRequest {
@NotBlank
private String projectPath;
@NotBlank
private String imageName;
private String dockerfilePath;
private Map<String, String> buildArgs;
private List<String> cacheFrom;
private boolean pushToRegistry;
private String registryUrl;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class OptimizedBuildRequest {
private String projectPath;
private String imageName;
private BuildStrategy optimizationStrategy;
private List<String> cacheFrom;
private Map<String, String> buildArgs;
private boolean securityScan;
private boolean sizeOptimization;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BuildResult {
private boolean success;
private String imageId;
private String errorMessage;
private long buildTime;
private long imageSize;
private ImageAnalysis imageAnalysis;
public static BuildResult success(String imageId, ImageAnalysis analysis) {
return BuildResult.builder()
.success(true)
.imageId(imageId)
.imageAnalysis(analysis)
.buildTime(System.currentTimeMillis())
.build();
}
public static BuildResult failure(String errorMessage) {
return BuildResult.builder()
.success(false)
.errorMessage(errorMessage)
.buildTime(System.currentTimeMillis())
.build();
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProjectAnalysis {
private ProjectType projectType;
private DependencyAnalysis dependencies;
private SizeMetrics sizeMetrics;
private List<Optimization> optimizations;
private boolean springBoot;
private boolean nativeCompatible;
private boolean compatibleWithDistroless;
public long getDependencySize() {
return dependencies != null ? dependencies.getEstimatedSize() : 0;
}
public boolean hasLayerSplittingOpportunities() {
return dependencies != null && dependencies.getTotalDependencies() > 10;
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DependencyAnalysis {
private List<DependencyInfo> dependencies;
private int totalDependencies;
private long estimatedSize;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DependencyInfo {
private String groupId;
private String artifactId;
private String version;
private String scope;
private long estimatedSize;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SizeMetrics {
private long sourceSize;
private long dependencySize;
private long buildOutputSize;
private long totalSize;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ImageAnalysis {
private long totalSize;
private int layerCount;
private List<LayerInfo> layers;
private String baseImage;
private boolean hasSecurityIssues;
private List<String> securityIssues;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LayerInfo {
private String id;
private long size;
private String command;
private boolean cacheable;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BuildReport {
private String imageId;
private SizeAnalysis sizeAnalysis;
private LayerAnalysis layerAnalysis;
private SecurityAnalysis securityAnalysis;
private List<OptimizationSuggestion> optimizationSuggestions;
private BuildStrategy recommendedStrategy;
}
public enum ProjectType {
SPRING_BOOT, QUARKUS, MICRONAUT, STANDARD_MAVEN, GRADLE, UNKNOWN
}
public enum BuildStrategy {
STANDARD_MULTI_STAGE,
DISTROLESS,
ALPINE,
NATIVE,
LAYER_OPTIMIZED,
MINIMAL_JRE
}
public enum Optimization {
DEPENDENCY_OPTIMIZATION,
DISTROLESS_BASE,
NATIVE_IMAGE,
LAYER_OPTIMIZATION,
CACHE_OPTIMIZATION,
SECURITY_HARDENING
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class DockerfileTemplate {
private String baseImage;
private List<BuildStage> buildStages;
private RuntimeConfig runtimeConfig;
private List<Optimization> optimizations;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class BuildStage {
private String name;
private String baseImage;
private String description;
private String workDir;
private List<String> commands;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class RuntimeConfig {
private String baseImage;
private String user;
private String workDir;
private List<String> environmentVariables;
private List<String> exposedPorts;
private String entrypoint;
private String healthCheck;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class BuildCommand {
private String dockerfileContent;
private String contextPath;
private String imageName;
private List<String> cacheFrom;
private Map<String, String> buildArgs;
private boolean noCache;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class BuildStrategyResult {
private BuildStrategy strategy;
private BuildResult result;
private long buildTime;
private long imageSize;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class ImageComparison {
private List<BuildStrategyResult> strategyResults;
private BuildStrategyResult bestStrategy;
public ImageComparison(List<BuildStrategyResult> strategyResults) {
this.strategyResults = strategyResults;
this.bestStrategy = findBestStrategy(strategyResults);
}
private BuildStrategyResult findBestStrategy(List<BuildStrategyResult> results) {
return results.stream()
.filter(result -> result.getResult().isSuccess())
.min(Comparator.comparingLong(BuildStrategyResult::getImageSize))
.orElse(null);
}
}
Build Optimization Controller
@RestController
@RequestMapping("/api/docker")
@Slf4j
public class DockerBuildController {
private final DockerBuildService dockerBuildService;
private final BuildOptimizationService optimizationService;
public DockerBuildController(DockerBuildService dockerBuildService,
BuildOptimizationService optimizationService) {
this.dockerBuildService = dockerBuildService;
this.optimizationService = optimizationService;
}
@PostMapping("/build/multi-stage")
public ResponseEntity<BuildResult> buildMultiStage(@Valid @RequestBody BuildRequest request) {
log.info("Building multi-stage image for project: {}", request.getProjectPath());
BuildResult result = dockerBuildService.buildMultiStageImage(request);
if (result.isSuccess()) {
return ResponseEntity.ok(result);
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
}
@PostMapping("/build/optimized")
public ResponseEntity<BuildResult> buildOptimized(@Valid @RequestBody OptimizedBuildRequest request) {
log.info("Building optimized image with strategy: {}", request.getOptimizationStrategy());
BuildResult result = dockerBuildService.buildOptimizedImage(request);
if (result.isSuccess()) {
return ResponseEntity.ok(result);
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
}
@GetMapping("/analyze/{projectPath}")
public ResponseEntity<ProjectAnalysis> analyzeProject(@PathVariable String projectPath) {
ProjectAnalysis analysis = optimizationService.analyzeProject(projectPath);
return ResponseEntity.ok(analysis);
}
@GetMapping("/compare/{projectPath}")
public ResponseEntity<ImageComparison> compareBuildStrategies(@PathVariable String projectPath) {
ImageComparison comparison = dockerBuildService.compareBuildStrategies(projectPath);
return ResponseEntity.ok(comparison);
}
@GetMapping("/report/{imageId}")
public ResponseEntity<BuildReport> getBuildReport(@PathVariable String imageId) {
BuildResult result = BuildResult.builder().imageId(imageId).build();
BuildReport report = optimizationService.generateBuildReport(result);
return ResponseEntity.ok(report);
}
@PostMapping("/optimize/{imageId}")
public ResponseEntity<BuildResult> optimizeExistingImage(@PathVariable String imageId) {
// This would implement image optimization techniques
return ResponseEntity.ok(BuildResult.success(imageId, null));
}
}
Docker Compose for Multi-Stage Builds
# docker-compose.yml version: '3.8' services: app: build: context: . dockerfile: Dockerfile.multistage image: my-app:latest ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=prod healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] interval: 30s timeout: 10s retries: 3 app-optimized: build: context: . dockerfile: Dockerfile.optimized image: my-app:optimized ports: - "8081:8080" app-distroless: build: context: . dockerfile: Dockerfile.distroless image: my-app:distroless ports: - "8082:8080" build-analyzer: build: context: . dockerfile: Dockerfile.analyzer volumes: - .:/project command: analyze /project
Best Practices and Optimization Tips
1. Layer Caching Optimization
# Optimized for layer caching FROM maven:3.9.4-eclipse-temurin-17 AS builder # Copy POM first for better caching WORKDIR /app COPY pom.xml . # Download dependencies (cached unless POM changes) RUN mvn dependency:go-offline -B # Copy source code (cached unless source changes) COPY src ./src # Build application RUN mvn clean package -DskipTests
2. Security Hardening
# Security-hardened multi-stage build FROM eclipse-temurin:17-jre-jammy # Update system and install security patches RUN apt-get update && \ apt-get upgrade -y && \ rm -rf /var/lib/apt/lists/* # Create non-root user RUN groupadd -r spring && useradd --no-log-init -r -g spring spring # Use non-privileged user USER spring # Copy application COPY --from=builder --chown=spring:spring /app/target/*.jar app.jar # Don't run as root USER spring
3. JVM Optimization
# JVM-optimized runtime FROM eclipse-temurin:17-jre-jammy # JVM optimization for containers ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:MaxGCPauseMillis=200" # Run application with optimized JVM settings ENTRYPOINT ["java", "-jar", "/app/app.jar"]
Performance Comparison
| Strategy | Image Size | Build Time | Security | Startup Time |
|---|---|---|---|---|
| Standard Multi-Stage | ~200MB | Medium | Good | Fast |
| Distroless | ~50MB | Medium | Excellent | Fast |
| Alpine | ~150MB | Fast | Good | Fast |
| Native Image | ~50MB | Slow | Excellent | Very Fast |
| Layer Optimized | ~180MB | Medium | Good | Fast |
Conclusion
Multi-stage builds optimization in Java provides:
- Significantly Smaller Images: From 600MB+ to 50-200MB
- Improved Security: Minimal attack surface with distroless images
- Better Build Performance: Layer caching and parallel builds
- Production Readiness: Optimized for container orchestration
- Cost Efficiency: Reduced storage and bandwidth costs
By implementing the strategies shown above, you can create highly optimized Docker images that are secure, fast, and efficient for production deployment. The combination of multi-stage builds, appropriate base images, and JVM optimization results in enterprise-grade containerized applications.
Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/
OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/
OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/
Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.
https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics
Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2
Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide
Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2
Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide
Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.
https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server
Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.