Docker Layer Caching in Java: Optimizing Build Performance

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:

  1. Multi-stage builds to separate build and runtime dependencies
  2. Optimal instruction ordering from least to most frequently changing
  3. Dependency layer separation for maximum cache utilization
  4. Build argument management for cache control
  5. Integration with build tools (Maven, Gradle)
  6. CI/CD pipeline optimization with cache persistence
  7. 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.

Leave a Reply

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


Macro Nepal Helper