M C

Loading

Blog

Cut Java and React CI Build Times with Docker BuildKit Registry Cache

A practical guide to persisting Docker BuildKit layers in a registry so disposable CI runners can reuse dependency and build layers for Spring Boot and React projects.

Cut Java and React CI Build Times with Docker BuildKit Registry Cache

Ephemeral CI workers start without a local Docker cache. That means a build can repeatedly download dependencies, compile code, and recreate intermediate layers even when most inputs have not changed. A Docker BuildKit registry cache addresses that problem by exporting cache data to a container registry after a build and importing it during later builds.

For Java and React projects, the most important design choice is usually Dockerfile order: resolve dependencies before copying frequently changing source code. The remote cache then has useful layers to reuse across separate CI workers.

Why registry cache works for disposable CI workers

BuildKit can use external cache backends to share build cache across environments. The registry backend stores cache data at an OCI registry reference, allowing later builds to import matching layers even when they run on a different worker. This makes it suitable for CI systems where the local builder is removed after each job. Docker’s registry cache documentation

Keep the cache distinct from the deployable application image. The application image represents a runnable artifact; the cache represents reusable build work. Separating them lets teams manage retention, permissions, and cleanup independently.

A remote cache is most useful when builds run often enough for cache downloads and storage to cost less than repeated dependency resolution and compilation. It may offer limited value for small images, infrequent builds, or jobs dominated by work Docker cannot cache, such as external test services.

Structure Dockerfiles around stable inputs

Docker evaluates build instructions in order. When an earlier instruction changes, later layers must be reconsidered. A broad COPY . . before dependency installation means an ordinary source edit can invalidate the dependency-install layer. Docker recommends copying dependency manifests first, installing dependencies, and only then copying the remaining application files. Docker build cache optimization guidance

Layer group Typical inputs Expected invalidation Purpose
Toolchain Base image and system packages Base-image or toolchain update Defines the build environment
Dependencies pom.xml, lockfiles, package manifests Dependency definition change Preserves dependency download work
Application Source code and resources Normal source change Limits rebuilds to compilation or bundling
Runtime Built artifact and runtime base Artifact or runtime-base change Keeps build tooling out of the runtime image

This does not eliminate invalidation. It makes invalidation match the input that changed. A dependency update should rebuild dependency resolution. A source edit should generally leave the dependency layer reusable.

Configure Buildx with a dedicated cache reference

Use --cache-from to import cache candidates and --cache-to to export cache records produced by the build. The following example pushes an application image and exports a separate registry cache:

Code
docker buildx build \
  --push \
  --tag registry.example.com/team/catalog:${GIT_SHA} \
  --cache-from type=registry,ref=registry.example.com/team/catalog:buildcache-main \
  --cache-to type=registry,ref=registry.example.com/team/catalog:buildcache-main,mode=max \
  .

BuildKit validates imported cache candidates against the current instructions and inputs. Importing a cache does not force BuildKit to reuse an incompatible layer.

The registry backend supports min and max export modes. Docker documents min as the default and max as the option that exports cache for intermediate stages. Multi-stage Java and frontend builds commonly benefit from mode=max because expensive work often occurs before the final runtime stage. Docker registry cache backend reference

Do not silently ignore cache-export failures as a default policy. A cache miss can be expected on a cold build, but authentication, repository, or export failures deserve visibility because they can remove the expected benefit of the CI configuration.

Spring Boot: retain reusable dependency work

Spring Boot supports layered JARs. Its documented layer model separates dependencies, the Spring Boot loader, snapshot dependencies, and application code. The jarmode=layertools extraction path can place those layers into a runtime image independently, allowing application changes to avoid disturbing unchanged dependency content. Spring Boot layered JAR documentation

The build stage should still expose Maven inputs before application source:

Code
# syntax=docker/dockerfile:1
FROM maven:3-eclipse-temurin-21 AS build
WORKDIR /workspace
COPY pom.xml .
COPY .mvn .mvn
COPY mvnw .
RUN --mount=type=cache,target=/root/.m2 ./mvnw -q -DskipTests dependency:go-offline
COPY src src
RUN --mount=type=cache,target=/root/.m2 ./mvnw -q -DskipTests package
RUN java -Djarmode=layertools -jar target/*.jar extract

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /workspace/dependencies/ ./
COPY --from=build /workspace/spring-boot-loader/ ./
COPY --from=build /workspace/snapshot-dependencies/ ./
COPY --from=build /workspace/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

This pattern uses two different cache mechanisms. The registry cache exports BuildKit layers between builders. The Maven cache mount can reuse downloaded Maven artifacts where builder storage persists, but a cache mount alone should not be treated as portable storage between isolated CI workers. BuildKit distinguishes local cache mounts from exported external cache data. Moby BuildKit documentation

React: make the lockfile the dependency boundary

For a React build, copy the package manifest and lockfile before the rest of the project. Run the package manager’s deterministic CI installation command, then copy source files and build. Docker’s cache guidance uses this ordering to prevent ordinary source changes from unnecessarily reinstalling dependencies. Docker dependency-layer guidance

Code
# syntax=docker/dockerfile:1
FROM node:22 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

Verify the build output directory before adopting the example. Different React toolchains can use different output paths. Add a focused .dockerignore so local dependency directories, generated output, logs, and version-control files do not enlarge the build context or cause unnecessary cache changes. Docker recommends .dockerignore for reducing context transfer and avoiding accidental invalidation. Docker build-context guidance

Choose cache references deliberately

A mutable cache reference such as buildcache-main can support builds from a protected main branch. Branch builds can import both a branch-specific cache and the main cache, while exporting to a branch-specific reference. This is a practical policy rather than a BuildKit requirement.

  • Import the branch cache first, followed by the main cache.
  • Restrict writes to shared cache references to trusted CI contexts.
  • Use separate cache namespaces when build architecture, operating-system base, or dependency ecosystem differs.
  • Create a new cache reference after an intentional broad change, such as a build-toolchain migration.
  • Set registry retention rules for cache references separately from release images.

The aim is compatibility, not convenience. A cache reference should represent builds that are likely to share valid layers.

Plan invalidation instead of disabling cache

A source-only change should normally rebuild application compilation and runtime assembly. A lockfile or pom.xml change should rebuild dependency resolution and later layers. A Dockerfile change affects that instruction and later instructions. A base-image update intentionally affects downstream layers.

Docker documents that changed earlier layers require rebuilding from that point onward. That behavior is why manifest-first Dockerfiles matter more than simply enabling a registry cache. Docker cache invalidation guidance

Dependency freshness should be handled through declared dependency inputs, lockfiles, controlled dependency updates, and base-image refresh policy. Caching accelerates known inputs; it should not define which dependency versions a project accepts.

Measure the full CI tradeoff

A registry cache exchanges registry storage and network transfer for reduced build computation and dependency downloading. Measure full job duration rather than only the docker buildx build step. Include cache import, cache export, image push, and relevant queue or transfer time.

Compare representative cold builds, source-only changes, dependency changes, Dockerfile changes, and base-image refreshes. Record total duration, cache-hit output, transferred bytes where available, and cache-related failures. Avoid promising a universal percentage improvement because build topology, dependency size, and registry proximity affect the result.

Google Cloud Build notes that retrieving cached images from a registry adds time while also recommending caching and smaller runtime images as ways to improve build performance. Google Cloud Build build-speed guidance

FAQ

Does a Docker BuildKit registry cache replace the production image?

No. The registry cache backend stores cache data separately from the output image artifact. Docker registry cache documentation

Should every project use mode=max?

No. Use it when reusable intermediate stages justify the additional cache export, storage, and transfer. Docker documents min as the default and max as the mode that includes intermediate-stage cache. Docker cache export modes

Can cache mounts replace a registry cache on disposable CI workers?

Not by themselves. Cache mounts are builder-local package caches, while exported external cache data is intended for reuse across builders. Moby BuildKit documentation

Sources

Editorial note: AI assisted with research and drafting. Sources were selected for verification.

Mohamed CHAMI — Full-Stack Developer

Full-Stack Developer & Solutions Architect · Casablanca, Morocco

8+ years building Java/Spring Boot/Angular enterprise solutions. Former Senior Software Engineer at NTT Data and Satec. Authorized Google Workspace and Microsoft 365 Partner for Morocco.

Who is Mohamed CHAMI?

LinkedIn · GitHub · Contact

Leave a Comment

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