Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Docker BuildKit and buildx

By Kokil Thapa | Last reviewed: September 2026

Docker BuildKit and buildx are the modern replacement for Docker's legacy builder. If your CI pipeline still runs plain docker build, you are likely waiting on serial layer steps, weak cache reuse, and no clean path to ARM images. BuildKit is the backend engine. buildx is the CLI front end that exposes builders, platforms, and advanced cache. On real client projects I ship with Docker on Ubuntu and GitLab CI, switching to BuildKit often cuts build time before we touch application code. This guide covers enablement, daily commands, multi-platform workflows, and the failures I see after deploy.

What is Docker BuildKit and why should you use it?

BuildKit is Docker's next-generation image builder. It replaced the classic builder's linear, single-threaded model. BuildKit parses your Dockerfile into a directed acyclic graph (DAG). Independent stages run in parallel. Cache mounts and secrets stay out of final image layers.

The legacy builder still works on many hosts. It is slower, offers fewer cache options, and lacks first-class multi-platform support. BuildKit has been the default builder in Docker Engine since version 23.0. If you run a current Engine release on Ubuntu 22.04 or 24.04, BuildKit is probably already active.

buildx is a Docker CLI plugin built on BuildKit. It manages named builders, drives multi-platform builds, and supports advanced output modes. You do not choose BuildKit or buildx for most tasks. You enable BuildKit as the engine and use buildx when you need builders beyond the default one.

Docker BuildKit and buildx Stackdocker buildxCLI pluginBuildKitBuild engineRegistryPush outputDockerfile DAGParallel stages and cache mountsLocal daemondocker loadOCI tarballtype=oci export
Docker BuildKit and buildx: the CLI plugin talks to BuildKit, which executes the Dockerfile graph and exports to a registry or local daemon.

BuildKit matters for three practical reasons on production pipelines:

  • Speed: Parallel stage execution and smarter layer caching shrink CI minutes. That saves money on GitLab runners and AWS build hosts.
  • Security: BuildKit supports RUN --mount=type=secret so API keys never land in image layers.
  • Portability: buildx can target linux/amd64 and linux/arm64 from one command. Useful for Apple Silicon dev laptops and Graviton EC2 nodes.

If you maintain Laravel or WordPress apps in containers, BuildKit pairs well with multi-stage Docker builds and image size reduction techniques. Smaller images deploy faster and reduce attack surface.

How do you enable Docker BuildKit and buildx on Ubuntu?

On Docker Engine 23.0 and later, BuildKit is the default builder. Verify your version first:

docker version --format '{{.Server.Version}}'
docker buildx version

If buildx is missing, install the plugin package on Ubuntu:

sudo apt-get update
sudo apt-get install -y docker-buildx-plugin docker-compose-plugin

Confirm BuildKit is active

Run a build with plain progress output. You should see BuildKit-style step IDs, not the legacy "Step 1/8" format:

DOCKER_BUILDKIT=1 docker build -t myapp:test .

On current Engine releases you can skip the environment variable. BuildKit runs by default. To force the legacy builder during a one-off test:

DOCKER_BUILDKIT=0 docker build -t myapp:legacy .

Create and select a buildx builder

The default builder works for single-platform local builds. For multi-platform work, create a dedicated builder with the docker-container driver:

docker buildx create --name multiarch --driver docker-container --use
docker buildx inspect --bootstrap

The bootstrap step pulls the BuildKit image and starts the builder container. List builders anytime:

docker buildx ls

I've set this up on shared EC2 hosts that run Deployer and GitLab CI for legal-tech portals. The builder lives alongside the app containers without replacing the production runtime. For server hardening around Docker itself, see Linux system administration practices that cover firewall rules, log rotation, and daemon updates.

How does Docker buildx differ from the classic docker build?

Both commands read a Dockerfile and produce an image. The difference is capability and defaults. Classic docker build talks to the local daemon builder. buildx routes work through a configurable BuildKit backend and adds export options the classic command lacks.

FeatureClassic docker builddocker buildx build
Default since Engine 23+Uses BuildKit backendUses BuildKit with explicit builder control
Multi-platform (--platform)Single platform onlyMultiple platforms in one command
Output destinationsLocal daemon image storeRegistry, local daemon, OCI dir, tar
Cache backendsLocal layer cacheLocal, registry, S3, GCS, Azure Blob
Build contextsLocal directory or Git URLLocal, Git, HTTP, Docker image contexts
docker buildx bakeNot availableHCL/YAML/JSON multi-target builds
Load into local daemonAutomaticRequires --load flag

The --load requirement catches many developers. A multi-platform build cannot load all architectures into the local daemon at once. buildx pushes to a registry by default when multiple platforms are requested.

Classic Build vs BuildKit ExecutionLegacy: SerialBuildKit: ParallelStage 1: depsStage 2: compileStage 3: assetsStage 4: runtimeStage 1: depsStage 2Stage 3Stage 4: runtimeOne stage at a timeIndependent stages overlapBuildKit cuts wall-clock time on multi-stage Dockerfiles
Classic Docker builds run stages sequentially; Docker BuildKit executes independent DAG branches in parallel.

For day-to-day single-platform work on a dev laptop, docker build is fine. Switch to buildx when CI needs registry cache, multiple architectures, or bake files. The official Docker BuildKit documentation lists every front-end syntax extension. The multi-platform build guide covers platform flags and QEMU setup.

How do you use buildx for multi-platform image builds?

Multi-platform builds produce one manifest list tag that resolves to the correct architecture at pull time. This is how you ship the same tag to x86 EC2 and ARM Graviton without maintaining separate Dockerfiles.

Single-platform build loaded locally

docker buildx build \
  --platform linux/amd64 \
  --tag myorg/myapp:1.4.0 \
  --load \
  .

Multi-platform build pushed to a registry

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag registry.example.com/myorg/myapp:1.4.0 \
  --push \
  .

Use a registry you control. For a private setup, read the self-hosted Docker registry guide. For choosing between GitLab, Docker Hub, and ECR, see the container registry comparison.

Detailed platform workflows live in the dedicated buildx multi-platform builds article. The short version: create a docker-container driver builder, bootstrap it, then push with --push.

BuildKit Dockerfile features worth adopting

BuildKit unlocks syntax that the legacy builder ignores or rejects. These patterns appear in production Laravel and Node images I maintain:

# syntax=docker/dockerfile:1

FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/cache \
    composer install --no-dev --prefer-dist

FROM node:26-alpine AS assets
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci
COPY . .
RUN npm run build

FROM php:8.3-fpm AS app
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
COPY . .

The # syntax=docker/dockerfile:1 directive tells BuildKit to use the current Dockerfile frontend. Cache mounts persist Composer and npm caches across builds without bloating image layers. That pairs directly with Docker layer caching strategies.

buildx Multi-Platform WorkflowGit pushCI triggerbuildx builderdocker-containerlinux/amd64linux/arm64RegistryManifest listProduction hosts pull one tagEC2 amd64 nodeGraviton arm64 nodeRuntime selects correct digest automatically
Docker buildx builds each platform slice, then publishes a single manifest list tag that amd64 and arm64 hosts pull correctly.

GitLab CI example with registry cache

This pattern mirrors pipelines I run for containerised Laravel apps. It uses buildx with registry-backed cache and pushes on main branch merges:

build-image:
  stage: build
  image: docker:27-cli
  services:
    - docker:27-dind
  variables:
    DOCKER_BUILDKIT: "1"
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
    - docker buildx create --use --name ci-builder
    - docker buildx inspect --bootstrap
  script:
    - |
      docker buildx build \
        --platform linux/amd64 \
        --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
        --tag $CI_REGISTRY_IMAGE:latest \
        --cache-from type=registry,ref=$CI_REGISTRY_IMAGE:buildcache \
        --cache-to type=registry,ref=$CI_REGISTRY_IMAGE:buildcache,mode=max \
        --push \
        .

The mode=max cache export stores intermediate layers remotely. Rebuilds after a small code change reuse Composer and npm layers. That is often the biggest CI win after enabling BuildKit itself.

How do you optimize BuildKit cache and bake files for CI?

Local layer cache disappears when CI runners are ephemeral. Registry cache backends solve that. buildx supports type=registry, type=local, and cloud object stores depending on your infrastructure.

Inline cache vs registry cache

  • Inline cache (--build-arg BUILDKIT_INLINE_CACHE=1): Embeds cache metadata in the image. Simple, but less efficient for large multi-stage files.
  • Registry cache (--cache-to/--cache-from type=registry): Stores cache layers in a dedicated tag. Best for GitLab CI and GitHub Actions.
  • Cache mounts (RUN --mount=type=cache): Speeds package manager steps inside a single build. Does not replace registry cache for cold runners.

docker buildx bake for multi-service repos

When a repo ships an app container, a queue worker, and an nginx sidecar, bake files beat shell loops. Create docker-bake.hcl:

group "default" {
  targets = ["app", "worker"]
}

target "app" {
  context    = "."
  dockerfile = "Dockerfile"
  tags       = ["myorg/myapp:latest"]
  platforms  = ["linux/amd64"]
}

target "worker" {
  context    = "."
  dockerfile = "Dockerfile.worker"
  tags       = ["myorg/myapp-worker:latest"]
  platforms  = ["linux/amd64"]
}

Run all targets in one command:

docker buildx bake --push

For local Laravel development, compare bake-driven builds against Laravel Sail and Docker or a hand-rolled Docker Compose multi-container setup. Sail abstracts BuildKit for dev. Production CI still benefits from explicit buildx control.

On a booking platform like Adventure Third Pole Trek, separate app and worker images let the web tier scale independently from queue consumers. bake keeps both images versioned and cached consistently.

What are common BuildKit and buildx problems in production?

BuildKit fails differently from the legacy builder. Error messages reference graph nodes and mount types. These are the issues I troubleshoot most often.

BuildKit Troubleshooting MapError: multiple platformsCannot --load multi-archFix: use --pushOr build one platformError: unknown flag mountLegacy builder activeFix: enable BuildKitAdd syntax directiveError: exec format errorWrong arch on hostFix: match platformCheck manifest listMost buildx errors are flag or platform mismatchesNot application bugs
Typical Docker BuildKit and buildx failures — multi-platform load limits, disabled BuildKit, and architecture mismatches — and their fixes.

Multi-platform load error

ERROR: multi-platform build is not supported for docker driver means you tried --load with multiple --platform values. Push to a registry instead, or build one platform at a time.

exec format error at runtime

The image architecture does not match the host CPU. An arm64 image on amd64 hardware produces this immediately. Inspect the manifest:

docker buildx imagetools inspect myorg/myapp:latest

Tag correctly with Docker image tagging strategies so CI never deploys the wrong digest.

BuildKit disabled in CI

Older CI templates export DOCKER_BUILDKIT=0. Remove that line. Confirm the Dockerfile starts with the syntax directive if you use --mount or --secret flags.

Stale builder state

Builders accumulate over months on shared servers. Prune safely:

docker buildx prune -f
docker buildx rm old-builder-name

If you evaluate alternatives, read the Podman vs Docker migration guide. Podman uses Buildah and a different build path. buildx skills still transfer because both ecosystems target OCI images.

For runtime routing after the image lands on a server, pair your pipeline with a reverse proxy like Traefik for Docker. BuildKit gets the image built. Traefik handles TLS and host rules at deploy time.

Key Takeaways

  • BuildKit is the default builder in Docker Engine 23+; install the buildx plugin for multi-platform and registry cache workflows.
  • Use docker build for quick single-platform local work; use docker buildx build --push when CI needs multiple architectures or remote cache.
  • Add # syntax=docker/dockerfile:1 and cache mounts to speed Composer, npm, and apt steps without fattening image layers.
  • Registry cache with --cache-to type=registry,mode=max gives ephemeral CI runners warm builds on the second pipeline run.
  • Multi-platform images require --push to a registry — you cannot --load more than one platform into the local daemon.
  • Inspect manifest lists with docker buildx imagetools inspect before blaming application code for exec format errors.

People Also Ask

Is BuildKit enabled by default in Docker?

Yes, on Docker Engine 23.0 and later. BuildKit runs as the default builder for docker build. You can confirm by watching for BuildKit-style progress output or by checking that DOCKER_BUILDKIT=1 is set in your environment. Older installations may still need that variable in /etc/docker/daemon.json or the shell profile.

Do I need buildx if I already have BuildKit?

For basic builds, no. You need buildx when you want named builders, multi-platform images, registry cache import and export, bake files, or output to OCI directories. buildx is the management layer; BuildKit is the engine underneath both docker build and docker buildx build.

Can buildx build images without pushing to a registry?

Yes, for single-platform builds. Pass --load to import the result into the local Docker image store. Multi-platform builds cannot load all architectures locally at once. Export to a tar with --output type=tar,dest=image.tar if you need offline artefacts without a registry.

Does buildx work with Docker Compose?

Compose v2 integrates BuildKit automatically when you run docker compose build. For advanced cache and platform flags, build images with buildx in CI and reference the tagged result in your Compose file. Mixing compose build and buildx push in the same pipeline is a common pattern on enterprise application projects.

Ship faster builds with BuildKit today

Docker BuildKit and buildx are not experimental extras in 2026. They are the baseline for any team building container images in CI. Enable the buildx plugin, create a builder, add registry cache to your pipeline, and adopt cache mounts in your Dockerfile. The first build may take the same time. The second build is where you win.

If your pipeline still runs on legacy patterns or your images keep failing with platform errors, I can audit the Dockerfile, CI config, and deploy path together. That is the same full-stack work I do across Laravel, Docker, and Linux hosting projects. For ongoing runner and daemon maintenance after the pipeline is fixed, see support and maintenance or contact us to discuss your build setup.

Frequently Asked Questions

Docker BuildKit is Docker's next-generation image builder. It parses a Dockerfile into a DAG, runs independent stages in parallel, and supports cache mounts and secrets that stay out of final image layers.

Yes. Docker Engine 23.0 and later use BuildKit as the default builder for docker build. Confirm with docker version and watch for BuildKit-style step IDs instead of legacy Step 1/8 output.

For basic single-platform docker build work, no. You need the buildx CLI plugin when you want named builders, multi-platform images, registry or cloud cache import and export, bake files, or output to OCI directories and tar archives. BuildKit is the engine; buildx is the management layer on top of it.

On Docker Engine 23.0 and later, BuildKit is already the default builder. Verify with docker version --format '{{.Server.Version}}' and docker buildx version. If buildx is missing, run sudo apt-get install -y docker-buildx-plugin docker-compose-plugin. For multi-platform work, create a builder with docker buildx create --name multiarch --driver docker-container --use, then docker buildx inspect --bootstrap. I've set this up on shared EC2 hosts running GitLab CI alongside production containers without changing the runtime stack.

Both read a Dockerfile and produce an image, but buildx routes work through a configurable BuildKit backend with explicit builder control. Classic docker build targets a single platform and loads into the local daemon automatically. buildx adds multi-platform builds, registry and object-store cache backends, Git and HTTP build contexts, and docker buildx bake for multi-target repos. The detail that trips most teams: multi-platform buildx builds require --push to a registry because you cannot --load more than one architecture into the local daemon at once.

Create a docker-container driver builder, bootstrap it, then run docker buildx build with --platform linux/amd64,linux/arm64, a registry tag, and --push. BuildKit builds each platform slice and publishes one manifest list tag that amd64 and arm64 hosts pull correctly. This is how you ship the same tag to x86 EC2 and ARM Graviton without separate Dockerfiles. For a single local architecture, use --platform linux/amd64 with --load instead.

Yes for single-platform builds: pass --load to import into the local Docker image store. Multi-platform builds cannot load all architectures locally at once, so push to a registry or export with --output type=tar,dest=image.tar for offline artefacts.

You tried --load with multiple --platform values. The local daemon can only hold one architecture at a time. Push to a registry with --push instead, or build one platform per command. On current Engine releases, multi-platform work also needs a docker-container driver builder created with docker buildx create --driver docker-container, not the default docker driver alone.

The pulled image architecture does not match the host CPU. An arm64 image on amd64 hardware fails immediately at runtime. Inspect the manifest with docker buildx imagetools inspect myorg/myapp:latest before blaming application code. Tag and deploy the correct digest for your target platform, and verify CI is building linux/amd64 for x86 EC2 or linux/arm64 for Graviton as intended.

Cache mounts use RUN --mount=type=cache in the Dockerfile with a syntax directive at the top: # syntax=docker/dockerfile:1. They persist Composer, npm, or apt caches across builds without writing those files into image layers. On production Laravel and Node images I maintain, this keeps vendor and node_modules steps warm while keeping final images smaller. Cache mounts speed package-manager steps inside one build; they do not replace registry cache for ephemeral CI runners that start cold every pipeline run.

Set DOCKER_BUILDKIT=1, log into your registry, create a CI builder with docker buildx create --use --name ci-builder, bootstrap it, then pass --cache-from type=registry,ref=$CI_REGISTRY_IMAGE:buildcache and --cache-to type=registry,ref=$CI_REGISTRY_IMAGE:buildcache,mode=max on docker buildx build before --push. mode=max stores intermediate layers remotely so a small code change reuses Composer and npm layers on the next run. This pattern mirrors pipelines I run for containerised Laravel apps on GitLab with docker:27-cli and docker:27-dind.

bake reads HCL, YAML, or JSON files such as docker-bake.hcl to build multiple targets in one command. Use it when a repo ships an app container, a queue worker, and sidecar images that should share cache and versioning. Define groups and targets with context, dockerfile, tags, and platforms, then run docker buildx bake --push. On a booking platform with separate web and worker images, bake keeps both builds consistent without shell loops around individual docker buildx build calls.

Use RUN --mount=type=secret so API keys and tokens are available only during a build step and never committed to a layer. This requires BuildKit enabled and the # syntax=docker/dockerfile:1 frontend directive. If CI still exports DOCKER_BUILDKIT=0, remove that line or secret mounts and other BuildKit-only syntax will fail with errors referencing unsupported mount types rather than clear credential warnings.

Compose v2 integrates BuildKit automatically when you run docker compose build. For advanced registry cache, multi-platform flags, or bake-driven multi-service builds, build and push images with buildx in CI, then reference the tagged result in your Compose file. Mixing compose build locally and buildx push in CI is a common pattern on enterprise application projects where dev simplicity and production pipeline control diverge.

Builders accumulate over months on shared hosts running Deployer and GitLab CI. List them with docker buildx ls, prune unused state with docker buildx prune -f, and remove named builders you no longer need using docker buildx rm old-builder-name. If builds behave oddly after upgrades, bootstrap a fresh docker-container driver builder and confirm BuildKit progress output instead of legacy step numbering. Pair image builds with runtime routing tools like Traefik after deploy; BuildKit handles the build, not TLS or host rules at runtime.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: