This note is the hub for four Java and Spring Boot Dockerfile variants, from a simple Alpine build to an aggressively optimized one. A contents table links each variant and gives its rough image size, and a comparison table scores them on build time, security, maintainability, debug capability, performance, and compliance. The middle of the note collects the techniques those variants share, such as multi-stage builds, BuildKit cache mounts, custom JREs, Spring Boot layers, and SHA256 pinning. Later sections turn that into guidance on when to pick each approach, plus security, performance, and build notes. A ten-point best practices checklist closes the page.
Overview
A detailed analysis of four Dockerfile optimization techniques for Java/Spring Boot applications, ranging from simple to extremely optimized in terms of size, security, and performance.
Contents
| # | Technique | Description | Size |
|---|---|---|---|
| 1 | Distroless with Custom JRE (jlink) and jdeps | Custom JRE + auto dependency analysis | ~150MB |
| 2 | Auto-dependency Update with Java Healthcheck | Auto update dependencies + Java healthcheck | ~200MB |
| 3 | Alpine with JVM Optimization | Alpine + JVM tuning + graceful shutdown | ~180MB |
| 4 | Ultra-optimized: Custom JRE + Spring Boot Layers + tini | Custom JRE + layers + tini + full compliance | ~80MB |
Comparison of Techniques
| Criterion | Distroless + jdeps | Auto-update | Alpine + JVM Opt | Ultra-optimized |
|---|---|---|---|---|
| Image size | Small (~150MB) | Medium (~200MB) | Medium (~180MB) | Very small (~80MB) |
| Build time | Long | Very long | Medium | Very long |
| Security | Excellent | Good | Good | Excellent |
| Maintainability | Hard | Very hard | Easy | Hard |
| Debug capability | No shell | No shell | Has shell | Limited |
| Performance | Good | Good | Very good | Excellent |
| Layer caching | Good | Good | Good | Excellent |
| Compliance | Good | Good | Good | Excellent |
Common Optimization Techniques
Multi-stage Build
- Purpose: Reduce the final image size by removing build dependencies
- How: Separate the builder stage from the production stage
- Benefit: The final image contains only runtime dependencies
- Best practice: Use at least 2 stages (build + runtime)
Layer Caching
- Purpose: Optimize build time by caching layers that do not change
- How: Copy the Gradle wrapper and build.gradle first, install dependencies, and only then copy the source code
- Benefit: When source code changes, dependencies do not need to be reinstalled
- Best practice: Copy files from least frequently changed to most frequently changed
BuildKit Cache Mount
- Purpose: Cache Gradle dependencies between builds
- How:
RUN --mount=type=cache,target=/root/.gradle - Benefit: Dependencies are cached, so builds are much faster
- Best practice: Use
sharing=lockedfor parallel builds
Custom JRE with jlink
- Purpose: Shrink the JRE by including only the required modules
- How: Use
jdepsto analyze dependencies, thenjlinkto create the custom JRE - Benefit: Reduces the JRE size from ~300MB to ~50-100MB
- Trade-off: Longer build time, and the module list must be maintained
Spring Boot Layers
- Purpose: Optimize Docker layer caching by splitting the JAR into layers
- How:
java -Djarmode=layertools -jar app.jar extract - Benefit: When application code changes, the dependencies layer does not need to be rebuilt
- Best practice: Copy layers from least frequently changed to most frequently changed
Non-root User
- Purpose: Improve security by running the application as a non-root user
- How: Create a user with
adduser, then use theUSERdirective - Benefit: Smaller attack surface, compliance with the CIS Docker Benchmark
- Best practice: Use a specific UID/GID (not a random one)
JVM Optimization Flags
- Purpose: Optimize JVM performance in containers
- Key flags:
-XX:+UseContainerSupport: Respect container memory limits-XX:MaxRAMPercentage=75.0: Use 75% of container memory for the heap-XX:+UseG1GC: G1 garbage collector (good for containers)-XX:+ExitOnOutOfMemoryError: Fail fast on OOM
- Benefit: Better memory management, predictable performance
Graceful Shutdown
- Purpose: Ensure the application shuts down gracefully when it receives SIGTERM
- How:
- Spring Boot:
-Dserver.shutdown=graceful - Use tini:
ENTRYPOINT ["/sbin/tini", "--"] STOPSIGNAL SIGTERM
- Spring Boot:
- Benefit: In-flight requests are not lost, and shutdown is clean
Healthcheck
- Purpose: Monitor application health
- Options:
- External tool:
wget,curl(must be installed) - Java class: Compile a Java class for the healthcheck
- Spring Boot Actuator: the
/healthendpoint
- External tool:
- Best practice: Use
--start-periodto give the app time to start
SHA256 Pinning
- Purpose: Ensure reproducible builds and security
- How: Pin base images with
@sha256:... - Benefit: Avoids supply chain attacks and gives reproducible builds
- Best practice: Always pin base images in production
OCI Labels
- Purpose: Metadata for traceability and compliance
- How: Use
org.opencontainers.image.*labels - Benefit: Easier image tracing and compliance with standards
- Best practice: Include version, source, license, and authors
Distroless Images
- Purpose: Minimize the attack surface by removing the shell and tools
- How: Use
gcr.io/distroless/*orscratch - Benefit: Extremely secure, with no shell to exploit
- Trade-off: Hard to debug, and external tools are needed for the healthcheck
When to Use Which Technique
Distroless + jdeps
- Production environments that need high security
- Microservices that need small images
- When you want automatic dependency analysis
Auto-update
- Development environments
- CI/CD pipelines with automated security scanning
- Projects that need frequent dependency updates
- Should not be used in production
Alpine + JVM Opt
- Production environments that need a balance between size and maintainability
- When debugging capabilities are needed
- Applications that need JVM tuning
- When you want shell access
Ultra-optimized
- Production environments that need maximum optimization
- High-scale deployments
- Compliance requirements
- When image size is a critical factor
Security Considerations
- Non-root user: Always run the application as a non-root user
- SHA256 pinning: Pin base images to avoid supply chain attacks
- Minimal base: Use distroless or Alpine to reduce the attack surface
- No unnecessary tools: Remove unneeded tools (shell, package manager)
- Security scanning: Scan images with security scanners (Trivy, Snyk)
- Dependency updates: Update dependencies regularly to fix vulnerabilities
Performance Considerations
- JVM flags: Tune JVM flags for containers
- Memory management: Use
UseContainerSupportandMaxRAMPercentage - GC tuning: Choose an appropriate GC (G1GC for containers)
- Layer caching: Optimize layer ordering to maximize cache hits
- BuildKit cache: Use cache mounts for Gradle dependencies
Build Optimization
- BuildKit cache mounts: Cache Gradle dependencies between builds
- Layer ordering: Copy files from least frequently changed to most frequently changed
- Parallel operations: Use
--parallelwhere possible - Skip tests:
-x testin production builds (run tests separately) - Cleanup: Remove build artifacts right after the build finishes
Best Practices Summary
- Always use multi-stage builds
- Pin base images with SHA256
- Use a non-root user
- Optimize layer caching
- Use BuildKit cache mounts
- JVM optimization flags for containers
- Graceful shutdown with tini
- Healthcheck for monitoring
- OCI labels for traceability
- Security scanning before deployment
References
- Spring Boot Docker Best Practices
- jlink Documentation
- jdeps Documentation
- Docker Best Practices
- CIS Docker Benchmark
See also: Distroless JLink · Alpine JVM · Ultra Optimized