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.