If you work with large Docker builds in production, especially with multi-module Spring Boot applications, you’ve probably suffered through this:
You change one tiny application.yaml.
Then you rebuild the image.
And suddenly Docker starts downloading half the internet again.
I recently faced this while working on a multi-module Spring Boot application with multiple pom.xml files and a huge dependency tree. Every rebuild felt painful. Sometimes the build would sit for 8–10 minutes just resolving Maven dependencies before even packaging the app.
The worst part?
The actual code change was tiny.
The Real Problem Wasn't Maven
It was the Docker layer strategy.
A lot of Dockerfiles are written like this:
COPY . .
RUN mvn package
Looks simple.
But this completely destroys Docker layer caching.
Every Docker instruction creates a separate immutable layer.
Each layer gets its own content hash internally. If any layer changes, Docker invalidates all layers beneath it and rebuilds them again.
So if your Dockerfile copies source code before resolving dependencies:
COPY src ./src
RUN mvn package
then every source code change forces:
- Maven dependency resolution
- Plugin downloads
- Packaging
- Recompilation
all over again.
Even though dependencies never changed.
That’s where most of the build time gets wasted.
The Optimization That Changed Everything
I switched to Docker BuildKit.
At the top of the Dockerfile:
# syntax=docker/dockerfile:1.4
This connects the Dockerfile frontend to the modern BuildKit backend and unlocks advanced features like cache mounts.
Then instead of doing a normal dependency resolution:
RUN mvn dependency:go-offline
I used:
RUN --mount=type=cache,target=/root/.m2 \
mvn dependency:go-offline
This was the game changer.
What --mount=type=cache Actually Does
Normally Maven stores dependencies inside:
/root/.m2
Without BuildKit:
- Dependencies download every fresh build
- Docker throws them away after the layer rebuilds
With BuildKit cache mounts:
- Docker creates a persistent cache directory on the host
- Maven dependencies stay cached
- Future builds instantly reuse them
So after the first build:
- Dependencies no longer redownload
- Rebuilds become dramatically faster
- Iterative development becomes smooth again
My rebuild time dropped from several minutes to just a few seconds.
The Other Optimization Most People Miss
Instruction ordering inside Dockerfiles matters a lot.
This pattern is extremely important:
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
Why?
Because pom.xml changes far less frequently than application source code.
Docker can now cache dependency resolution separately from source changes.
So:
- Changing Java code only rebuilds packaging layers
- Dependency layers remain untouched
- Rebuilds stay fast
If you reverse the order:
COPY src ./src
COPY pom.xml .
then every source code change invalidates all subsequent layers.
That’s catastrophic for large Java builds.
But Wait… How Does This Work in CI/CD?
At this point I had another question myself.
If CI runners like GitHub Actions use fresh ephemeral machines every run, then where is the cache actually stored?
Because after every pipeline:
- Runner gets destroyed
- Filesystem disappears
-
.m2cache disappears too
So how does caching survive across builds?
The answer is BuildKit remote cache export/import.
In GitHub Actions, you can persist BuildKit cache across runners like this:
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
tags: myimage:latest
cache-from: type=gha
cache-to: type=gha,mode=max
This is insanely powerful.
What happens here:
-
cache-topushes BuildKit cache to GitHub's ephemeral cache storage -
cache-fromrestores it in future workflow runs
So even though every runner is brand new:
- Maven dependencies stay cached
- Docker layers stay reusable
- Builds remain fast across pipelines
This is how modern production CI/CD systems optimize container builds at scale.
Why This Matters in Production
This isn’t just about developer convenience.
In production engineering environments this directly impacts:
- CI/CD speed
- Deployment frequency
- Compute costs
- Feedback loops
- Developer productivity
For teams deploying dozens or hundreds of services, shaving even 5 minutes off builds compounds into massive engineering efficiency gains.
Especially in:
- Microservice architectures
- Monorepos
- Multi-module Maven projects
- Kubernetes delivery pipelines
- High-frequency deployment environments
Final Takeaway
If your Docker builds are painfully slow, don’t immediately blame:
- Maven
- Spring Boot
- Network latency
Most of the time the real issue is poor Docker layer design and missing cache strategy.
A properly structured Dockerfile combined with BuildKit caching can reduce rebuild times from 10 minutes to a few seconds.
And once you experience that speed difference, there’s no going back.
`
Top comments (0)