DEV Community

Satyaki
Satyaki

Posted on

Docker Builds Were Taking 10 Minutes. This One Change Brought It Down to Seconds

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

I used:

RUN --mount=type=cache,target=/root/.m2 \
    mvn dependency:go-offline
Enter fullscreen mode Exit fullscreen mode

This was the game changer.

What --mount=type=cache Actually Does

Normally Maven stores dependencies inside:

/root/.m2
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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
  • .m2 cache 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
Enter fullscreen mode Exit fullscreen mode

This is insanely powerful.

What happens here:

  • cache-to pushes BuildKit cache to GitHub's ephemeral cache storage
  • cache-from restores 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.


`
Enter fullscreen mode Exit fullscreen mode

Top comments (0)