How to use BuildKit and remote cache to speed up Docker builds in CI pipelines

Docker & Containers · By · Published on · Updated on · 9 min read

Three large parallel metal pipes crossing an area with trees
Visual metaphor for CI pipelines and build flow Photo: Wolfgang Weiser / Pexels

This guide shows how to use BuildKit and remote cache to speed up Docker builds in CI pipelines. You will enable Buildx, prepare a Dockerfile to take advantage of dependency caches, and configure a registry-backed remote cache in CI providers. The goal is to reduce run time while keeping reproducibility and security.

Prerequisites and an overview of BuildKit in CI

BuildKit is Docker’s build engine offering parallelism, fine-grained cache control and features like cache mounts and secrets. In ephemeral CI runners the local cache is lost between runs. For that reason we use a remote cache to reuse layers across executions.

You need a recent Docker with Buildx. If needed, see How to Install Docker and Docker Compose on Linux (/en/install-docker-docker-compose-linux). If you use multistage builds, this guide complements Create Lightweight Docker Images with Multistage (/en/lightweight-docker-images-multistage).

A registry-backed remote cache stores metadata and layers as OCI artifacts. It works on any CI that can log in and push to a registry. Do not store secrets in the cache; use BuildKit secret mounts when required.

  • An account on a registry with push/pull permissions.
  • A CI runner with Docker available (with or without DinD).
  • A Dockerfile compatible with BuildKit and preferably multistage.
  • Network access from the CI to the image registry.

1) Enable BuildKit and Buildx locally

Enable BuildKit and Buildx to test the flow locally before moving it to CI. Create a dedicated builder and bootstrap the driver. This ensures you’ll see the same logs and behavior as in the pipeline.

Then run a simple build and confirm Buildx is in use. Use plain progress to see cache hits clearly in the logs.

# Ativar BuildKit na sessão atual
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1

docker buildx version

# Criar e usar um builder baseado em container
docker buildx create --name ci-builder --use
# Inicializar capacidades (QEMU, cache driver, etc.)
docker buildx inspect --bootstrap

# Build de teste com saída detalhada
docker buildx build --progress=plain -t test-image .

2) Prepare the Dockerfile to leverage cache mounts

To benefit from remote cache, minimize invalidations and use cache mounts for steps that download dependencies. With BuildKit, RUN accepts --mount=type=cache to persist directories between builds.

Include the Dockerfile syntax directive for recent features and isolate dependencies in stages. Avoid copying the entire repository before install. Never write secrets into layers; use secret mounts when needed.

  • COPY manifests selectively before running install.
  • Separate build and production dependencies.
  • Avoid invalidations by placing rarely-changed files first.
  • Enable --mount=type=cache for package managers.
# syntax=docker/dockerfile:1.6
FROM node:20-slim AS base
WORKDIR /app

# 1) Dependências para build (usa cache do npm)
FROM base AS deps
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

# 2) Build do app
FROM base AS build
COPY --from=deps /app/node_modules /app/node_modules
COPY . .
RUN --mount=type=cache,target=/root/.npm \
    npm run build

# 3) Dependências de produção (sem dev)
FROM base AS prod-deps
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --omit=dev

# 4) Runtime enxuto
FROM node:20-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]

3) Configure remote cache via registry with Buildx

In ephemeral runners use a registry-backed remote cache. Buildx can export and import cache with --cache-to and --cache-from. The cache is referenced using a reserved tag.

Authenticate to the registry before the build. Use mode=max to write the most metadata possible and improve hits. Ensure the cache tag is stable so it can be reused by subsequent builds.

# Variáveis de exemplo (ajuste para seu registry)
export REGISTRY=registry.example.com
export IMAGE_NAME=myapp
export IMAGE_TAG=sha-$(git rev-parse --short HEAD)
export IMAGE=$REGISTRY/$IMAGE_NAME:$IMAGE_TAG
export CACHE_REF=$REGISTRY/$IMAGE_NAME:buildcache

# Login no registry
echo "$REG_PASS" | docker login "$REGISTRY" -u "$REG_USER" --password-stdin

# Build e push com cache remoto
docker buildx build \
  --builder ci-builder \
  --progress=plain \
  --cache-from=type=registry,ref=$CACHE_REF \
  --cache-to=type=registry,ref=$CACHE_REF,mode=max \
  -t $IMAGE \
  --push \
  .

4) Example in GitHub Actions with registry cache

In GitHub Actions, use actions to set up Buildx, authenticate to the registry and run the build with cache-from/to. Keep the cache tag stable and use the commit SHA for the image tag.

If you don’t have a base pipeline yet, see How to set up an automatic deploy pipeline (/en/automatic-deploy-pipeline-github-actions), and for PHP examples, CI/CD with GitHub Actions: PHP/Laravel guide (/en/ci-cd-github-actions-php-laravel).

name: docker-build
on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Login registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push with registry cache
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
          cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
          cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max

5) Example in GitLab CI (or CI with Docker-in-Docker)

In pipelines that use Docker-in-Docker enable the dind service, disable TLS in the runner when appropriate and use Buildx to export and import cache to the project registry.

Authentication uses secure CI variables. Do not print tokens in logs. Keep a stable cache reference to maximize reuse across branches.

stages: [build]

variables:
  DOCKER_TLS_CERTDIR: ""
  DOCKER_HOST: tcp://docker:2375
  DOCKER_DRIVER: overlay2

services:
  - name: docker:24.0-dind

build-image:
  image: docker:24.0
  stage: build
  script:
    - apk add --no-cache curl
    - echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
    - docker buildx create --use --name ci-builder
    - docker buildx build --progress=plain \
        --cache-from=type=registry,ref=$CI_REGISTRY_IMAGE:buildcache \
        --cache-to=type=registry,ref=$CI_REGISTRY_IMAGE:buildcache,mode=max \
        -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA \
        --push \
        .
  only:
    - main

6) How to confirm the remote cache is working

Run a build to warm the cache then repeat changing only non-critical files. You should see "CACHED" or steps fast-skipped in the logs when using --progress=plain.

Also check total time and transfer size. If using different builders, point explicitly with --builder to avoid creating unnecessary parallel caches.

  • Use --progress=plain for detailed logs.
  • Validate that the cache-ref points to the correct tag.
  • Confirm you are logged in to the registry before the build.
# 1) Aquecer cache
docker buildx build --builder ci-builder \
  --cache-to=type=registry,ref=$CACHE_REF,mode=max \
  -t $IMAGE --push --progress=plain .

# 2) Segunda execução deve mostrar hits de cache
docker buildx build --builder ci-builder \
  --cache-from=type=registry,ref=$CACHE_REF \
  -t $IMAGE --push --progress=plain .

7) Common problems and how to resolve them

Cache misses on dependency install? Check that manifest files are copied before the rest of the code. For npm, package-lock.json must be stable.

Changes in ARG or ENV near the top of the Dockerfile invalidate subsequent layers. Pin versions and order instructions by stability.

On runners that wipe everything, remote cache is essential. docker buildx prune removes caches; run it only when necessary and with parameters that preserve needed data.

  • Organize the Dockerfile by layer stability.
  • Pin base images and tool versions.
  • Avoid dynamic ARGs at the top of the file.
  • Keep a single cache reference per service.
# Limpeza de caches (use com cautela)
# Isso apaga caches locais do builder selecionado
# Confirme o builder atual com: docker buildx ls

docker buildx prune --all --force

Frequently asked questions

Are BuildKit and Buildx the same thing?

No. BuildKit is the build engine. Buildx is the CLI that exposes BuildKit advanced features like multi-platform builds, different drivers and remote cache. You use Buildx to drive BuildKit with flags like --cache-from and --cache-to.

When should I use remote cache instead of local cache?

Use remote cache in CI pipelines with ephemeral runners where the local cache is lost between executions. Registry-backed cache lets you reuse layers across jobs and branches, reducing build time and bandwidth. For local development, local cache is usually sufficient.

Does the remote cache store build secrets?

Not if you avoid writing secrets into layers. Use BuildKit secret mounts to pass tokens at build time without persisting them in the image or cache. Also avoid echoing secrets to CI logs.

How do I handle a monorepo with multiple services?

Create a cache reference per service, using a stable cache tag per folder. This prevents collisions and maximizes reuse. Standardize Dockerfiles with multistage and cache mounts, and share a pipeline template across services.

Why do my installs still miss cache even with cache-to/from?

Most often it's invalidation due to instruction order or unstable manifests. Copy only package*.json or the lockfile before the rest of the code, pin versions, and use --mount=type=cache for the package manager. Check logs with --progress=plain to find the first miss.

Do I need to publish the final image to use remote cache?

No. You can export only the cache to the registry without publishing a public image tag. However, many pipelines also push the built image; you can reuse the same authentication to push and consume the cache.

Bring remote cache to your CI today

Implement BuildKit and remote cache in your pipeline and compare build times. Then complement with multistage builds and Dockerfile best practices.

Explore more guides

See also