Optimizing Spring Boot Docker Image Build, Push, and Pull with Layer Caching
This article explains how to efficiently build, push, and pull Docker images for Spring Boot applications by leveraging Docker's layered cache, separating dependency JARs from the application JAR, and measuring the performance improvements in a Kubernetes micro‑service environment.
In the era of Spring Cloud micro‑services, the deployment model has shifted from packaging each service as a JAR to packaging each service as a Docker image managed by Kubernetes. This article explores how to write an optimal Dockerfile for a Spring Boot project, measure build, push, and pull times, and improve performance using Docker's layer caching.
System Environment
Docker version: 18.09.3
Base image: openjdk:8u212-b04-jre-slim
Test registry: Alibaba Cloud Docker Hub
Project source: GitHub
1. Building a Standard Spring Boot Docker Image
Prepare a Maven‑built Spring Boot JAR (≈70 MB) and place it in the Docker build context.
FROM openjdk:8u212-b04-jre-slim
VOLUME /tmp
ADD target/*.jar app.jar
RUN sh -c 'touch /app.jar'
ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai"
ENV APP_OPTS=""
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS"]Build the image:
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 .The build completes in about 14 seconds. Push the image:
$ time docker push registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1Push finishes in roughly 25 seconds. Pull the image on another host:
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1Pull takes about 28 seconds.
2. Docker Layer Caching and Its Impact
Docker stores each instruction in a Dockerfile as a separate layer. If a layer’s command and its inputs do not change, Docker reuses the cached layer, dramatically reducing build time and network traffic.
Key commands that affect caching are RUN , ADD , and COPY . Understanding their behavior is essential for optimizing image builds.
3. Reducing Image Bloat by Splitting Dependencies
The Spring Boot JAR contains a BOOT-INF/lib directory with many dependency JARs (≈74 MB). By configuring Maven to keep dependencies outside the executable JAR and only include compiled classes, the application JAR shrinks to a few hundred kilobytes.
<build>
<plugins>
<!-- Maven JAR plugin to add classpath pointing to external lib folder -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
<!-- Spring Boot plugin configured to exclude all dependencies from the fat JAR -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>
</plugin>
<!-- Maven Dependency plugin to copy all external JARs to target/lib -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>After rebuilding, the application JAR is tiny, while the lib folder holds the large dependencies.
4. Modified Dockerfile Using Separate lib Layer
FROM openjdk:8u212-b04-jre-slim
VOLUME /tmp
COPY target/lib/ ./lib/
ADD target/*.jar app.jar
RUN sh -c 'touch /app.jar'
ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai"
ENV APP_OPTS=""
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS"]Placing the COPY target/lib/ ./lib/ instruction before adding the application JAR ensures the dependency layer is cached across builds when dependencies do not change.
5. Performance Results
First build (no cache) takes ~23 seconds. Subsequent builds after only changing the application code reuse the cached dependency layer and finish in ~2.5 seconds. Push of the unchanged layers completes in ~0.2 seconds, and pull of the new image takes ~2 seconds, demonstrating the dramatic speedup.
The approach shows that separating rarely‑changed dependencies from frequently‑changed application code can reduce network traffic from dozens of megabytes to a few hundred kilobytes, making Docker image workflows much more efficient for Spring Boot micro‑services.
In production, where image versions change slowly, this method can be adopted after thorough validation to ensure compatibility with existing deployment pipelines.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.