A Docker build can become slow when it repeatedly downloads the same dependencies. This often happens in CI/CD pipelines, where runners may start with an empty local cache for every job.
For example, a Node.js application may need to download hundreds of npm packages. A Python application may need to install packages from a requirements file, while a .NET application may restore NuGet dependencies. If the build environment does not retain useful caches, these operations can consume time on every run.
Docker BuildKit provides cache mounts that let build commands reuse downloaded data across builds. Instead of downloading dependencies again whenever a build step runs, the command can use files stored in a persistent cache managed by BuildKit.
Cache mounts are different from ordinary Docker layer caching. Understanding that difference helps developers configure faster builds without accidentally reusing stale dependencies or assuming that a cache will always exist.
What Is Docker BuildKit?
BuildKit is Docker's build engine. It supports features such as parallel build operations, improved build caching, secret mounts, and cache mounts.
A Dockerfile can use a cache mount with the RUN --mount=type=cache instruction.
For example:
# 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
The cache mount makes the npm cache directory available while npm ci runs. Package downloads can be reused by later builds that access the same BuildKit cache.
The directory is not committed to the resulting image layer as ordinary application content. This helps keep the final image focused on the files required to run the application.
The Dockerfile syntax directive enables the Dockerfile frontend version that supports the mount syntax. Use a supported version that matches your build environment.
Docker Layer Cache vs. Cache Mounts
Docker already caches build layers. However, layer caching and cache mounts solve different problems.
Layer caching allows Docker to reuse the result of a completed build instruction when the instruction and its relevant inputs have not changed.
Cache mounts preserve selected data used by build commands, even when a command needs to run again.
Consider this instruction:
COPY package.json package-lock.json ./
RUN npm ci
If the dependency files remain unchanged and Docker can reuse the existing layer, the npm ci instruction may not run at all.
However, if package-lock.json changes, Docker may invalidate that layer and execute npm ci again. Without a reusable package cache, npm may need to download many packages again.
A cache mount helps in this situation because the package manager can reuse cached download data even though the build instruction must execute.
The distinction is important: a cache mount does not guarantee that dependency installation will be skipped. It can make the installation faster when the command needs to run again.
Step 1: Enable a Cache Mount for npm
For a Node.js application, the npm download cache is a useful starting point.
# 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
The mount target, /root/.npm, is the npm cache directory for a container running as the root user.
When npm downloads packages, it can store reusable data in this directory. A later build using the same BuildKit cache can reuse that data.
The npm ci command installs dependencies according to the lockfile. It is generally a good choice for automated builds because it expects the lockfile to match the dependency manifest.
If the container runs as a non-root user, verify the correct npm cache directory and adjust the mount target accordingly.
Also, do not copy the entire project into the image before installing dependencies unless the application requires it. Copying dependency manifests first can help Docker reuse the dependency-installation layer when unrelated source files change.
Step 2: Cache Python Package Downloads
Python projects can use the same approach with pip.
# syntax=docker/dockerfile:1
FROM python:3.13 AS build
WORKDIR /app
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-compile -r requirements.txt
COPY . .
CMD ["python", "app.py"]
The cache mount points to pip's default cache directory for the root user. Downloaded packages and other reusable data can remain available to later builds using the same BuildKit cache.
The --no-compile option avoids generating Python bytecode during installation. Whether this is appropriate depends on the application's deployment requirements.
This example is a simple application image. Production Dockerfiles may use a virtual environment, a separate dependency stage, or another package-management workflow.
The important point is to mount the cache directory used by the package manager rather than an arbitrary directory that the tool does not use.
Step 3: Cache NuGet Packages in .NET Builds
.NET projects often restore NuGet packages before compilation. A cache mount can reduce repeated downloads during container builds.
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet restore
COPY . ./
RUN dotnet publish \
-c Release \
-o /app/publish \
--no-restore
The example assumes that the project file is in the build context root. Projects with multiple project files or a solution should copy the relevant project files before restoring dependencies.
The cache mount targets the usual NuGet global packages directory for the root user. If the container uses another user or a custom NuGet configuration, verify the actual package directory.
The dotnet restore command resolves and downloads the project's dependencies. The later dotnet publish --no-restore command avoids repeating the restore operation when the required assets are already available.
The cache mount does not replace the restore step. It makes package data reusable when restore must run again.
Step 4: Use Cache Mounts in CI/CD
A cache mount is most useful when the BuildKit cache survives between builds. On a persistent build machine, the same builder may retain cache data for later jobs.
In CI/CD, however, a runner may be temporary. If every workflow creates a new isolated builder, its cache mounts may not contain data from earlier runs.
This is why adding a cache mount alone does not guarantee faster builds in every CI environment.
For example, a GitHub Actions workflow can build the image with BuildKit:
name: Build Container
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: myapp:ci
cache-from: type=gha
cache-to: type=gha,mode=max
This workflow uses Buildx and GitHub Actions cache export and import to help preserve build cache data between workflow runs.
The cache-from setting imports available cache data, while cache-to exports cache data after the build. The mode=max setting can export intermediate build stages as well as the final stage, which is useful for multi-stage builds.
The example uses action version tags for readability. In production, review third-party actions and pin them to verified full commit SHAs according to your organization's security policy.
The effectiveness of this setup depends on the runner, cache backend, cache availability, and the way the build is configured. Cache limits, eviction, and changes to cache keys or build inputs can also affect reuse.
Step 5: Understand Cache Invalidation
Cache mounts are an optimization, not a source of truth. A build must remain correct even when the cache is empty or has been removed.
For dependency installation, use lockfiles and package-manager commands that resolve dependencies according to the project's intended versions.
For example, npm projects should normally commit package-lock.json and use npm ci in automated builds. Python and .NET projects should use suitable dependency constraints or lockfiles where the project workflow supports them.
Do not use a cache mount to preserve generated application output that must be rebuilt when source files change. Build outputs generally belong in ordinary build steps and layers with appropriate inputs.
A useful rule is:
Use layer caching to reuse completed build operations.
Use cache mounts to reuse downloaded packages or intermediate data used by those operations.
Use lockfiles and explicit build inputs to control dependency resolution.
Make sure a clean build succeeds without any cache.
This approach keeps the build reproducible while allowing it to benefit from cached data.
Common Mistakes to Avoid
Mounting the wrong directory
A cache mount only helps when the build tool actually uses the mounted directory. Check the package manager's cache location, especially when running as a non-root user.
Assuming every CI runner keeps its cache
A new builder may start without previous cache data. Configure an appropriate cache backend or reuse a persistent builder where suitable.
Depending on cache contents for correctness
Cache data can disappear at any time. A build that fails when its cache is empty is relying on the cache incorrectly.
Caching dependencies without using a lockfile
A package cache does not guarantee that dependency versions remain consistent. Use the dependency-management features appropriate for your ecosystem.
Ignoring cache security
Build caches may contain package data, generated files, or other intermediate artifacts. Avoid sharing caches between trusted release builds and untrusted jobs when that could expose sensitive data or allow cache poisoning. Review the cache backend's access controls and isolation behavior.
Expecting a cache mount to reduce the final image size
A cache mount primarily reduces repeated work during builds. It does not automatically reduce the size of the final image. Use multi-stage builds, appropriate runtime base images, and careful file selection to control image size.
Best Practices for Production Builds
Start by identifying which build steps consume the most time. Package installation and dependency restoration are good candidates for cache mounts, but measuring the actual pipeline is better than adding caches everywhere.
Keep dependency manifests separate from frequently changing source files so Docker can reuse dependency layers where possible. Combine this with cache mounts for the package-manager data that remains useful when those layers are invalidated.
Use a supported BuildKit frontend and configure cache import and export in CI/CD. Keep the cache scoped appropriately, and make sure clean builds continue to work.
Finally, review build logs and timings after introducing caching. Cache performance depends on the project, network, runner, and dependency set. The goal is to reduce unnecessary work without weakening dependency management or build reliability.
Conclusion
Docker BuildKit cache mounts can reduce repeated dependency downloads in Docker builds. They are particularly useful when source changes invalidate a dependency-installation layer but the package manager can still reuse previously downloaded data.
For Node.js, Python, and .NET projects, the process is similar: identify the package manager's cache directory, mount it during the relevant build command, and configure CI/CD to preserve reusable BuildKit cache data.
Cache mounts work best alongside normal layer caching, lockfiles, and reproducible build steps. Treat them as a performance optimization rather than a requirement, and your builds can remain reliable even when the cache is unavailable.
Join the conversation! Your thoughts help the community grow.