Skip to content
Go back

Multi-Platform Docker Builds with buildx

By SumGuy 5 min read
Multi-Platform Docker Builds with buildx

You built an image on your M3 Mac. Works great locally. Ship it to your x86_64 server and it crashes with exec format error. Your binary is ARM64, but the server is Intel.

Enter buildx. It’s Docker’s multi-platform build tool, and it’s been in the core since Docker 19.03. You just probably haven’t used it yet.

What Is buildx?

buildx is a BuildKit frontend that lets you build images for multiple architectures (ARM64, AMD64, ARM, 386, etc.) from a single machine. It uses QEMU emulation or actual hardware builders to cross-compile.

Most of the time you don’t need it. Your CI/CD system can build on the actual hardware. But when you’re developing locally or need to build everything at once, buildx is a lifesaver.

Check If You Have It

Terminal window
docker buildx version
# github.com/docker/buildx v0.12.1

It’s been default in Docker Desktop for a couple years. If you don’t have it, upgrade Docker.

Enable Buildkit

Make sure BuildKit is enabled:

Terminal window
export DOCKER_BUILDKIT=1
docker build --help | grep platform
# --platform value Set the target platform for the build

Or set it permanently:

~/.docker/daemon.json
{
"features": {
"buildkit": true
}
}

The Basic Workflow

  1. Create a builder instance (one-time setup)
  2. Build for multiple platforms
  3. Push the multi-arch manifest to a registry

Step 1: Create a builder

Terminal window
docker buildx create --name multi-platform-builder --use
docker buildx inspect --bootstrap

The --use flag sets it as default. --bootstrap starts it.

Step 2: Build and push

Terminal window
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
--tag myregistry/myimage:latest \
.

That’s it. Docker builds for both architectures and pushes to the registry. On pull, Docker automatically picks the right one.

Key Flags

—platform linux/amd64,linux/arm64

Comma-separated list of architectures to build. Common combinations:

Full list: docker buildx ls --builder multi shows available platforms.

—push

Push to registry after build. Without this, the image stays local (if you’re using docker builder, not docker-container).

—output type=oci,dest=./output

Export to local directory instead of pushing. Useful for local testing:

Terminal window
docker buildx build \
--platform linux/amd64,linux/arm64 \
--output type=oci,dest=./build \
.
# Images end up in ./build/, load them with:
docker load < build/index.json

—cache-from

Reuse cache from a registry image to speed up builds:

Terminal window
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
--cache-from type=registry,ref=myregistry/myimage:latest \
--tag myregistry/myimage:latest \
.

Dockerfile Considerations

Most Dockerfiles work as-is. But some things break with cross-compilation:

Alpine as base: Generally works

FROM alpine:latest
RUN apk add --no-cache curl

Ubuntu with compiled binaries: Use multiarch base images

# Good for multi-arch
FROM ubuntu:22.04
# If you're installing pre-compiled binaries, make sure the base image supports them
# ubuntu:22.04 has multiarch support built-in

Your own compiled binaries: Can be tricky

If your Dockerfile compiles a binary with RUN go build ..., the binary is compiled for the build platform, not the target platform. Use FROM --platform=$BUILDPLATFORM to fix:

# Build stage uses your native architecture
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "Building for $TARGETPLATFORM on $BUILDPLATFORM"
WORKDIR /app
COPY . .
# Use CGO_ENABLED=0 for static binaries that work everywhere
RUN CGO_ENABLED=0 go build -o server .
# Final stage runs on target architecture
FROM alpine:latest
COPY --from=builder /app/server /app/server
ENTRYPOINT ["/app/server"]

The ARG TARGETPLATFORM and ARG BUILDPLATFORM are automatically set by buildx.

Python/Node/Java: Usually fine as-is

These languages run on the target platform, not compiled:

FROM python:3.11-slim
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

This works across architectures without change.

Local Testing

Can’t easily test ARM64 on your Mac without buildx, but you can check the image exists:

Terminal window
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
--tag myregistry/myimage:test-build \
.
# Then on your server:
docker pull myregistry/myimage:test-build
docker run myregistry/myimage:test-build

Or use QEMU emulation (slow, but works):

Terminal window
docker run --rm --privileged tonistiigi/binfmt --install all
# Now you can run ARM images on x86:
docker run --platform linux/arm64 --rm myimage:test uname -m
# aarch64

When You Actually Need buildx

You need it if:

You don’t need it if:

A Real Workflow

GitHub Actions:

name: Build Multi-Arch
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/${{ github.repository }}:latest

One pipeline, two architectures, one manifest. Done.

buildx is one of those tools that sits unused until you suddenly need it at 2 AM. But when that day comes, you’ll be very glad it exists.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
Why kill -9 Is the Wrong Default
Next Post
The umask You've Been Ignoring

Related Posts