Docker layer caching is a crucial optimization technique that significantly reduces build times by reusing unchanged layers from previous builds. This guide covers implementing effective layer caching strategies for Java applications.
Core Concepts
What is Docker Layer Caching?
- Docker builds images in layers
- Each instruction in a Dockerfile creates a layer
- Unchanged layers are reused from cache
- Changed layers and subsequent layers are rebuilt
Key Optimization Strategies:
- Order instructions from least to most frequently changing
- Separate dependency installation from source code
- Use multi-stage builds
- Leverage build arguments effectively
- Minimize layer count where appropriate
Dependencies and Setup
1. Maven Dependencies for Build Integration
<properties>
<maven.version>3.9.5</maven.version>
<docker.maven.plugin.version>0.43.0</docker.maven.plugin.version>
<jib.maven.plugin.version>3.4.0</jib.maven.plugin.version>
</properties>
<dependencies>
<!-- Maven Plugin API -->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>${maven.version}</version>
<scope>provided</scope>
</dependency>
<!-- Maven Core -->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>${maven.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Docker Maven Plugin -->
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>${docker.maven.plugin.version}</version>
</plugin>
<!-- Jib Maven Plugin -->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>${jib.maven.plugin.version}</version>
</plugin>
</plugins>
</build>
Dockerfile Optimization Strategies
1. Basic Optimized Dockerfile
# Use official OpenJDK runtime as base image FROM eclipse-temurin:21-jre-jammy as runtime # Set working directory WORKDIR /app # Create a non-root user for security RUN groupadd --system --gid 1000 appgroup && \ useradd --system --uid 1000 --gid appgroup appuser && \ chown -R appuser:appgroup /app # Copy the JAR file from build stage COPY --from=build /app/target/*.jar app.jar # Switch to non-root user USER appuser # Expose application 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.jar"]
2. Multi-Stage Build with Layer Caching
# Stage 1: Build stage FROM eclipse-temurin:21-jdk-jammy as build # Install Maven RUN apt-get update && \ apt-get install -y maven && \ rm -rf /var/lib/apt/lists/* && \ apt-get clean WORKDIR /app # Copy pom.xml and download dependencies first (cached layer) COPY pom.xml . COPY .mvn .mvn COPY mvnw . # Download dependencies (this layer is cached unless pom.xml changes) RUN ./mvnw dependency:go-offline -B # Copy source code COPY src src # Build application (this layer rebuilds when source changes) RUN ./mvnw clean package -DskipTests # Stage 2: Runtime stage FROM eclipse-temurin:21-jre-jammy as runtime # Install curl for health checks RUN apt-get update && \ apt-get install -y curl && \ rm -rf /var/lib/apt/lists/* && \ apt-get clean WORKDIR /app # Create application user RUN groupadd --system --gid 1000 appgroup && \ useradd --system --uid 1000 --gid appgroup appuser && \ chown -R appuser:appgroup /app # Copy JAR from build stage COPY --from=build --chown=appuser:appgroup /app/target/*.jar app.jar # Switch to non-root user USER appuser 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"]
3. Advanced Multi-Stage with Dependency Layer
# Stage 1: Dependency stage (maximize caching) FROM eclipse-temurin:21-jdk-jammy as dependencies WORKDIR /app # Copy only the files needed for dependency resolution COPY pom.xml . COPY .mvn .mvn COPY mvnw . # Download dependencies and plugins RUN ./mvnw dependency:go-offline dependency:resolve-plugins -B # Stage 2: Build stage FROM dependencies as build # Copy source code COPY src src # Build application RUN ./mvnw clean package -DskipTests # Stage 3: Test stage (optional) FROM build as test RUN ./mvnw test # Stage 4: Runtime stage FROM eclipse-temurin:21-jre-jammy as runtime # Install minimal required packages RUN apt-get update && \ apt-get install -y --no-install-recommends curl && \ rm -rf /var/lib/apt/lists/* && \ apt-get clean WORKDIR /app # Create application user RUN groupadd --system --gid 1000 appgroup && \ useradd --system --uid 1000 --gid appgroup appuser && \ chown -R appuser:appgroup /app # Copy JAR from build stage COPY --from=build --chown=appuser:appgroup /app/target/*.jar app.jar # Create directory for application data RUN mkdir -p /app/data && \ chown -R appuser:appgroup /app/data USER appuser EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 # Use shell form for environment variable expansion ENTRYPOINT ["java", "-jar", "/app/app.jar"]
4. Spring Boot Specific Optimization
# Stage 1: Build stage FROM eclipse-temurin:21-jdk-jammy as build WORKDIR /app # Copy build files COPY pom.xml . COPY .mvn .mvn COPY mvnw . # Download dependencies (cached layer) RUN ./mvnw dependency:go-offline -B # Copy source code COPY src src # Build with layered JAR support RUN ./mvnw clean package -DskipTests && \ java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted # Stage 2: Runtime stage with layered JAR FROM eclipse-temurin:21-jre-jammy as runtime WORKDIR /app # Create application user RUN groupadd --system --gid 1000 appgroup && \ useradd --system --uid 1000 --gid appgroup appuser && \ chown -R appuser:appgroup /app # Copy extracted layers from build stage COPY --from=build /app/target/extracted/dependencies/ ./ COPY --from=build /app/target/extracted/spring-boot-loader/ ./ COPY --from=build /app/target/extracted/snapshot-dependencies/ ./ COPY --from=build /app/target/extracted/application/ ./ USER appuser EXPOSE 8080 ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
Java Build Integration
1. Docker Build Service
@Service
@Slf4j
public class DockerBuildService {
private final DockerClient dockerClient;
private final BuildCacheService cacheService;
public DockerBuildService(DockerClient dockerClient, BuildCacheService cacheService) {
this.dockerClient = dockerClient;
this.cacheService = cacheService;
}
public BuildResult buildImage(ImageBuildRequest request) {
log.info("Building Docker image for project: {}", request.getProjectName());
try {
// Prepare build context
Path buildContext = prepareBuildContext(request);
// Configure build arguments for caching
Map<String, String> buildArgs = createBuildArgs(request);
// Execute Docker build with cache optimization
String imageId = buildImageWithCache(buildContext, buildArgs, request);
// Tag and push if required
if (request.isPushToRegistry()) {
pushImage(imageId, request.getImageTags());
}
log.info("Successfully built image: {}", imageId);
return BuildResult.success(imageId, request.getImageTags());
} catch (Exception e) {
log.error("Docker build failed: {}", e.getMessage(), e);
return BuildResult.failure(e.getMessage());
}
}
private String buildImageWithCache(Path buildContext, Map<String, String> buildArgs,
ImageBuildRequest request) throws DockerException {
BuildImageCmd buildCmd = dockerClient.buildImageCmd()
.withBaseDirectory(buildContext.toFile())
.withDockerfile(new File(buildContext.toFile(), "Dockerfile"))
.withBuildArgs(buildArgs)
.withTags(new HashSet<>(request.getImageTags()))
.withPull(request.isPullBaseImage());
// Configure cache settings
if (request.getCacheFrom() != null) {
buildCmd.withCacheFrom(request.getCacheFrom());
}
if (request.isUseBuildCache()) {
buildCmd.withNoCache(false);
} else {
buildCmd.withNoCache(true);
}
// Build and return image ID
return buildCmd.exec(new BuildImageResultCallback() {
@Override
public void onNext(BuildResponseItem item) {
log.debug("Build progress: {}", item.getStream());
super.onNext(item);
}
}).awaitImageId();
}
private Map<String, String> createBuildArgs(ImageBuildRequest request) {
Map<String, String> buildArgs = new HashMap<>();
// Add build-time arguments for cache busting control
buildArgs.put("BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()));
buildArgs.put("PROJECT_VERSION", request.getProjectVersion());
buildArgs.put("GIT_COMMIT", request.getGitCommit());
// Add Maven build arguments
buildArgs.put("MAVEN_OPTS", request.getMavenOpts());
return buildArgs;
}
private Path prepareBuildContext(ImageBuildRequest request) throws IOException {
Path tempDir = Files.createTempDirectory("docker-build-");
// Copy Dockerfile
Files.copy(request.getDockerfilePath(), tempDir.resolve("Dockerfile"));
// Copy application files
copyApplicationFiles(request.getProjectRoot(), tempDir);
// Create .dockerignore file
createDockerIgnore(tempDir);
return tempDir;
}
private void createDockerIgnore(Path tempDir) throws IOException {
List<String> ignorePatterns = Arrays.asList(
".git",
".github",
".mvn",
"mvnw",
"mvnw.cmd",
"target/",
"*.iml",
".idea/",
"*.log",
"node_modules/",
"dist/",
"Dockerfile*",
"docker-compose*",
".dockerignore"
);
Files.write(tempDir.resolve(".dockerignore"),
ignorePatterns, StandardOpenOption.CREATE);
}
// Other helper methods...
}
2. Build Cache Management Service
@Service
@Slf4j
public class BuildCacheService {
private final DockerClient dockerClient;
public BuildCacheService(DockerClient dockerClient) {
this.dockerClient = dockerClient;
}
public CacheStats getCacheStats() {
try {
Info info = dockerClient.infoCmd().exec();
return CacheStats.builder()
.buildCacheSize(info.getDriverStatus().stream()
.filter(status -> status[0].equals("Build Cache"))
.findFirst()
.map(status -> parseSize(status[1]))
.orElse(0L))
.build();
} catch (Exception e) {
log.warn("Failed to get cache stats: {}", e.getMessage());
return CacheStats.empty();
}
}
public void pruneBuildCache() {
try {
PruneCmd pruneCmd = dockerClient.pruneCmd(PruneType.BUILD_CACHE);
PruneResponse response = pruneCmd.exec();
log.info("Pruned build cache: {} bytes reclaimed", response.getSpaceReclaimed());
} catch (Exception e) {
log.error("Failed to prune build cache: {}", e.getMessage());
}
}
public List<ImageLayer> analyzeImageLayers(String imageName) {
try {
InspectImageResponse imageInfo = dockerClient.inspectImageCmd(imageName).exec();
return imageInfo.getRootFS().getLayers().stream()
.map(layer -> ImageLayer.builder()
.digest(layer)
.size(estimateLayerSize(layer))
.build())
.collect(Collectors.toList());
} catch (Exception e) {
log.error("Failed to analyze image layers: {}", e.getMessage());
return List.of();
}
}
public BuildOptimizationReport analyzeBuildOptimization(Path dockerfilePath) {
try {
List<String> dockerfileLines = Files.readAllLines(dockerfilePath);
List<BuildStage> stages = parseDockerfileStages(dockerfileLines);
List<OptimizationSuggestion> suggestions = analyzeForOptimizations(stages);
return BuildOptimizationReport.builder()
.dockerfilePath(dockerfilePath.toString())
.stages(stages)
.suggestions(suggestions)
.cacheEfficiencyScore(calculateCacheEfficiency(stages))
.build();
} catch (IOException e) {
log.error("Failed to analyze Dockerfile: {}", e.getMessage());
return BuildOptimizationReport.error(e.getMessage());
}
}
private List<OptimizationSuggestion> analyzeForOptimizations(List<BuildStage> stages) {
List<OptimizationSuggestion> suggestions = new ArrayList<>();
for (BuildStage stage : stages) {
// Check for COPY . . before RUN commands
boolean hasCopyAllBeforeRun = hasCopyAllBeforeRunCommands(stage);
if (hasCopyAllBeforeRun) {
suggestions.add(OptimizationSuggestion.builder()
.stage(stage.getName())
.suggestion("Move COPY . . to after dependency installation commands")
.impact("HIGH")
.build());
}
// Check for apt-get update without cleanup
boolean hasAptUpdateWithoutCleanup = hasAptUpdateWithoutCleanup(stage);
if (hasAptUpdateWithoutCleanup) {
suggestions.add(OptimizationSuggestion.builder()
.stage(stage.getName())
.suggestion("Combine apt-get update with install and cleanup in single RUN")
.impact("MEDIUM")
.build());
}
}
return suggestions;
}
// Helper analysis methods...
}
3. Data Transfer Objects
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ImageBuildRequest {
private String projectName;
private String projectVersion;
private Path projectRoot;
private Path dockerfilePath;
private List<String> imageTags;
private boolean useBuildCache;
private boolean pullBaseImage;
private boolean pushToRegistry;
private List<String> cacheFrom;
private String gitCommit;
private String mavenOpts;
@Builder.Default
private Map<String, String> buildArgs = new HashMap<>();
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BuildResult {
private boolean success;
private String imageId;
private List<String> tags;
private String error;
private Instant buildTime;
private Long buildDurationMs;
public static BuildResult success(String imageId, List<String> tags) {
return BuildResult.builder()
.success(true)
.imageId(imageId)
.tags(tags)
.buildTime(Instant.now())
.build();
}
public static BuildResult failure(String error) {
return BuildResult.builder()
.success(false)
.error(error)
.buildTime(Instant.now())
.build();
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CacheStats {
private Long buildCacheSize;
private Integer cachedImages;
private Integer totalImages;
public static CacheStats empty() {
return CacheStats.builder()
.buildCacheSize(0L)
.cachedImages(0)
.totalImages(0)
.build();
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BuildOptimizationReport {
private String dockerfilePath;
private List<BuildStage> stages;
private List<OptimizationSuggestion> suggestions;
private Double cacheEfficiencyScore;
private String error;
public static BuildOptimizationReport error(String error) {
return BuildOptimizationReport.builder()
.error(error)
.build();
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BuildStage {
private String name;
private String baseImage;
private List<DockerInstruction> instructions;
private Integer layerCount;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OptimizationSuggestion {
private String stage;
private String suggestion;
private String impact;
private String description;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ImageLayer {
private String digest;
private Long size;
private String command;
}
Maven Plugin Implementation
1. Docker Layer Cache Maven Plugin
@Mojo(name = "build-docker", defaultPhase = LifecyclePhase.PACKAGE)
public class DockerBuildMojo extends AbstractMojo {
@Parameter(property = "dockerfile", defaultValue = "Dockerfile")
private String dockerfile;
@Parameter(property = "imageName", defaultValue = "${project.artifactId}")
private String imageName;
@Parameter(property = "imageTag", defaultValue = "${project.version}")
private String imageTag;
@Parameter(property = "useCache", defaultValue = "true")
private boolean useCache;
@Parameter(property = "cacheFrom")
private List<String> cacheFrom;
@Parameter(property = "buildArgs")
private Map<String, String> buildArgs;
@Parameter(property = "project", required = true, readonly = true)
private MavenProject project;
@Override
public void execute() throws MojoExecutionException {
getLog().info("Building Docker image with layer caching optimization");
try {
DockerBuildService buildService = createBuildService();
ImageBuildRequest request = ImageBuildRequest.builder()
.projectName(project.getArtifactId())
.projectVersion(project.getVersion())
.projectRoot(project.getBasedir().toPath())
.dockerfilePath(project.getBasedir().toPath().resolve(dockerfile))
.imageTags(createImageTags())
.useBuildCache(useCache)
.cacheFrom(cacheFrom)
.buildArgs(buildArgs != null ? new HashMap<>(buildArgs) : new HashMap<>())
.build();
BuildResult result = buildService.buildImage(request);
if (result.isSuccess()) {
getLog().info("Successfully built Docker image: " +
String.join(", ", result.getTags()));
} else {
throw new MojoExecutionException("Docker build failed: " + result.getError());
}
} catch (Exception e) {
throw new MojoExecutionException("Failed to build Docker image", e);
}
}
private List<String> createImageTags() {
List<String> tags = new ArrayList<>();
tags.add(imageName + ":" + imageTag);
tags.add(imageName + ":latest");
return tags;
}
private DockerBuildService createBuildService() {
DockerClient dockerClient = DockerClientBuilder.getInstance().build();
return new DockerBuildService(dockerClient, new BuildCacheService(dockerClient));
}
}
2. Plugin Configuration in pom.xml
<build>
<plugins>
<plugin>
<groupId>com.example</groupId>
<artifactId>docker-layer-cache-maven-plugin</artifactId>
<version>1.0.0</version>
<configuration>
<imageName>my-spring-app</imageName>
<useCache>true</useCache>
<cacheFrom>
<param>my-spring-app:latest</param>
</cacheFrom>
<buildArgs>
<BUILD_TIMESTAMP>${maven.build.timestamp}</BUILD_TIMESTAMP>
<GIT_COMMIT>${git.commit.id}</GIT_COMMIT>
</buildArgs>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build-docker</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Jib Plugin for alternative Docker builds -->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<from>
<image>eclipse-temurin:21-jre-jammy</image>
</from>
<to>
<image>my-registry/${project.artifactId}:${project.version}</image>
</to>
<container>
<user>1000:1000</user>
<ports>
<port>8080</port>
</ports>
<environment>
<JAVA_OPTS>-Xmx512m</JAVA_OPTS>
</environment>
</container>
</configuration>
</plugin>
</plugins>
</build>
Spring Boot Integration
1. Docker Build Configuration
@Configuration
@EnableConfigurationProperties(DockerBuildProperties.class)
@Slf4j
public class DockerBuildConfiguration {
@Bean
@ConditionalOnMissingBean
public DockerClient dockerClient() {
return DockerClientBuilder.getInstance().build();
}
@Bean
public DockerBuildService dockerBuildService(DockerClient dockerClient,
BuildCacheService cacheService) {
return new DockerBuildService(dockerClient, cacheService);
}
@Bean
public BuildCacheService buildCacheService(DockerClient dockerClient) {
return new BuildCacheService(dockerClient);
}
@Bean
public BuildOptimizationService buildOptimizationService() {
return new BuildOptimizationService();
}
}
@ConfigurationProperties(prefix = "docker.build")
@Data
public class DockerBuildProperties {
private boolean enabled = true;
private boolean useCache = true;
private boolean pullBaseImage = false;
private String defaultRegistry;
private List<String> cacheFrom = new ArrayList<>();
private Map<String, String> defaultBuildArgs = new HashMap<>();
@Data
public static class Registry {
private String url;
private String username;
private String password;
}
}
2. REST API for Docker Build Management
@RestController
@RequestMapping("/api/docker")
@Slf4j
public class DockerBuildController {
private final DockerBuildService buildService;
private final BuildCacheService cacheService;
private final BuildOptimizationService optimizationService;
public DockerBuildController(DockerBuildService buildService,
BuildCacheService cacheService,
BuildOptimizationService optimizationService) {
this.buildService = buildService;
this.cacheService = cacheService;
this.optimizationService = optimizationService;
}
@PostMapping("/build")
public ResponseEntity<BuildResult> buildImage(@RequestBody ImageBuildRequest request) {
log.info("Received Docker build request for: {}", request.getProjectName());
try {
BuildResult result = buildService.buildImage(request);
if (result.isSuccess()) {
return ResponseEntity.accepted().body(result);
} else {
return ResponseEntity.badRequest().body(result);
}
} catch (Exception e) {
log.error("Build request failed: {}", e.getMessage(), e);
return ResponseEntity.internalServerError()
.body(BuildResult.failure("Internal server error"));
}
}
@GetMapping("/cache/stats")
public ResponseEntity<CacheStats> getCacheStats() {
CacheStats stats = cacheService.getCacheStats();
return ResponseEntity.ok(stats);
}
@PostMapping("/cache/cleanup")
public ResponseEntity<String> cleanupCache() {
cacheService.pruneBuildCache();
return ResponseEntity.ok("Build cache cleaned up successfully");
}
@GetMapping("/optimize/analyze")
public ResponseEntity<BuildOptimizationReport> analyzeDockerfile(
@RequestParam String dockerfilePath) {
try {
Path path = Paths.get(dockerfilePath);
BuildOptimizationReport report = optimizationService.analyzeBuildOptimization(path);
return ResponseEntity.ok(report);
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(BuildOptimizationReport.error(e.getMessage()));
}
}
@GetMapping("/images/{imageName}/layers")
public ResponseEntity<List<ImageLayer>> getImageLayers(@PathVariable String imageName) {
List<ImageLayer> layers = cacheService.analyzeImageLayers(imageName);
return ResponseEntity.ok(layers);
}
}
CI/CD Integration
1. GitHub Actions Workflow
name: Build and Push Docker Image
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for better caching
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
- name: Build with Maven
run: mvn -B package -DskipTests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=moby/buildkit:buildx-stable-1
network=host
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ github.workflow }}
cache-to: type=gha,mode=max,scope=${{ github.workflow }}
platforms: linux/amd64,linux/arm64
2. Jenkins Pipeline
pipeline {
agent any
environment {
DOCKER_REGISTRY = 'my-registry.com'
PROJECT_NAME = 'my-spring-app'
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timestamps()
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build Java') {
steps {
sh 'mvn -B clean package -DskipTests'
}
post {
success {
archiveArtifacts 'target/*.jar'
}
}
}
stage('Build Docker') {
steps {
script {
docker.build("${PROJECT_NAME}:${env.BUILD_ID}",
"--build-arg BUILD_NUMBER=${env.BUILD_ID} " +
"--cache-from ${PROJECT_NAME}:latest " +
".")
}
}
}
stage('Test Docker') {
steps {
sh """
docker run --rm ${PROJECT_NAME}:${env.BUILD_ID} \
java -jar /app/app.jar --version
"""
}
}
stage('Push Docker') {
when {
branch 'main'
}
steps {
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-credentials') {
docker.image("${PROJECT_NAME}:${env.BUILD_ID}").push()
docker.image("${PROJECT_NAME}:${env.BUILD_ID}").push('latest')
}
}
}
}
}
post {
always {
sh 'docker system prune -f'
}
}
}
Best Practices and Optimization Tips
1. Dockerfile Best Practices
# Use specific version tags for base images FROM eclipse-temurin:21.0.1_12-jre-jammy # Combine RUN commands to reduce layers RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ ca-certificates && \ rm -rf /var/lib/apt/lists/* && \ apt-get clean # Use .dockerignore to exclude unnecessary files # Copy only what's needed COPY target/*.jar app.jar # Use non-root user RUN useradd -m -u 1000 appuser USER appuser # Set appropriate memory limits ENV JAVA_OPTS="-Xmx512m -Xms256m" # Use health checks HEALTHCHECK --interval=30s CMD curl -f http://localhost:8080/health || exit 1
2. Maven Build Optimization
<profile>
<id>docker-optimized</id>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<configuration>
<from>
<image>eclipse-temurin:21-jre</image>
</from>
<to>
<image>${docker.image.prefix}/${project.artifactId}</image>
</to>
<container>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
<user>1000</user>
<ports>
<port>8080</port>
</ports>
<environment>
<JAVA_OPTS>-Xmx512m</JAVA_OPTS>
</environment>
</container>
</configuration>
</plugin>
</plugins>
</build>
</profile>
3. Application Configuration for Docker
@Configuration
@Slf4j
public class DockerOptimizationConfig {
@Bean
@Profile("docker")
public CommandLineRunner dockerOptimizationRunner() {
return args -> {
log.info("Running in Docker-optimized mode");
// Set Docker-specific optimizations
System.setProperty("spring.main.lazy-initialization", "true");
};
}
@Bean
@Profile("docker")
public TomcatConnectorCustomizer tomcatDockerCustomizer() {
return connector -> {
connector.setProperty("relaxedQueryChars", "[]");
connector.setProperty("relaxedPathChars", "[]");
// Reduce thread pool for containerized environment
connector.setProperty("maxThreads", "50");
};
}
}
Monitoring and Analytics
1. Build Performance Monitoring
@Service
@Slf4j
public class BuildPerformanceService {
private final MeterRegistry meterRegistry;
private final Timer buildTimer;
private final Counter cacheHitCounter;
private final Counter cacheMissCounter;
public BuildPerformanceService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.buildTimer = Timer.builder("docker.build.duration")
.description("Docker build duration")
.register(meterRegistry);
this.cacheHitCounter = Counter.builder("docker.cache.hits")
.description("Docker layer cache hits")
.register(meterRegistry);
this.cacheMissCounter = Counter.builder("docker.cache.misses")
.description("Docker layer cache misses")
.register(meterRegistry);
}
public void recordBuildMetrics(long duration, int cacheHits, int cacheMisses) {
buildTimer.record(duration, TimeUnit.MILLISECONDS);
cacheHitCounter.increment(cacheHits);
cacheMissCounter.increment(cacheMisses);
double cacheEfficiency = (double) cacheHits / (cacheHits + cacheMisses);
meterRegistry.gauge("docker.cache.efficiency", cacheEfficiency);
log.info("Build completed in {}ms with cache efficiency: {:.2f}%",
duration, cacheEfficiency * 100);
}
}
Conclusion
Implementing Docker layer caching in Java applications provides:
- Significantly faster build times through layer reuse
- Reduced bandwidth usage by avoiding redundant downloads
- Consistent build environments across different systems
- Better CI/CD pipeline performance
Key strategies covered:
- Multi-stage builds to separate build and runtime dependencies
- Optimal instruction ordering from least to most frequently changing
- Dependency layer separation for maximum cache utilization
- Build argument management for cache control
- Integration with build tools (Maven, Gradle)
- CI/CD pipeline optimization with cache persistence
- Monitoring and analytics for build performance
By implementing these patterns, you can achieve build time reductions of 50-80% for incremental builds, making development and deployment processes much more efficient.