Cada vez hay más servidores y equipos ARM (instancias arm64 en la nube, Apple Silicon, Raspberry Pi), y una imagen construida en un servidor x86_64 no se ejecuta en ellos. Docker Buildx, la interfaz de BuildKit, construye una misma imagen para varias arquitecturas y la publica bajo una sola etiqueta: cada máquina descarga automáticamente la variante que le corresponde. En este tutorial prepararás un servidor Ubuntu 24.04 para construir imágenes linux/amd64 y linux/arm64, compilarás una aplicación de Go con compilación cruzada y publicarás la imagen en un registro.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM y un usuario no root con privilegios sudo.
  • Docker Engine instalado desde el repositorio oficial de Docker, que incluye el plugin docker-buildx-plugin.
  • Tu usuario en el grupo docker, o anteponer sudo a los comandos docker.
  • Una cuenta en un registro de contenedores (Docker Hub en esta guía, con tu usuario your_user). Las imágenes multiarquitectura se publican en un registro, no se guardan como una sola imagen local.

Comprueba que Buildx está disponible:

docker buildx version
github.com/docker/buildx v0.17.1 257815a

Cómo funciona una imagen multiarquitectura

Una etiqueta multiarquitectura, como nginx:alpine, no apunta a una imagen sino a un índice (manifest list) que contiene una imagen por plataforma. Al hacer docker pull, el cliente elige la entrada que coincide con su arquitectura.

Buildx tiene dos formas de producir la variante de otra arquitectura:

MétodoCómo funcionaCuándo usarlo
Emulación con QEMUEjecuta las instrucciones RUN de la otra arquitectura mediante un emuladorCuando no puedes cambiar el proceso de build; es lento en pasos de compilación
Compilación cruzadaEl compilador se ejecuta de forma nativa y genera binarios para la arquitectura de destinoGo, Rust, .NET y otros lenguajes que lo admiten; mucho más rápido

En este tutorial configurarás QEMU (necesario en cualquier caso para instrucciones como apk add en la imagen final) y usarás compilación cruzada para la parte pesada.

Paso 1: Registrar los emuladores de QEMU

Para que el kernel pueda ejecutar binarios arm64 en un servidor x86_64, hay que registrar los emuladores de QEMU en binfmt_misc. La forma que documenta Docker es la imagen tonistiigi/binfmt:

docker run --privileged --rm tonistiigi/binfmt --install arm64
installing: arm64 OK
{
  "supported": [
    "linux/amd64",
    "linux/arm64",
    "linux/386"
  ],
  "emulators": [
    "qemu-aarch64"
  ]
}

Usa --install all si necesitas más arquitecturas, como arm/v7 o riscv64. Este registro no sobrevive a un reinicio del servidor. Si quieres que sea permanente, instala el paquete de Ubuntu, que registra los emuladores al arrancar mediante systemd-binfmt:

sudo apt install qemu-user-static

Comprueba que el emulador de arm64 está registrado:

ls /proc/sys/fs/binfmt_misc/ | grep aarch64
qemu-aarch64

Paso 2: Crear un builder con el controlador docker-container

El builder por defecto de Docker (controlador docker) no puede exportar imágenes multiplataforma con el almacén de imágenes clásico. Crea un builder nuevo con el controlador docker-container, que ejecuta BuildKit en un contenedor aparte, y márcalo como el builder activo:

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

Lista los builders y las plataformas que admite cada uno:

docker buildx ls
NAME/NODE         DRIVER/ENDPOINT                   STATUS    BUILDKIT   PLATFORMS
multiarch*        docker-container
 \_ multiarch0     \_ unix:///var/run/docker.sock   running   v0.16.0    linux/amd64*, linux/arm64*, linux/386
default           docker
 \_ default        \_ default                       running   v0.16.0    linux/amd64, linux/386

El asterisco junto al nombre indica el builder activo. linux/arm64 aparece en la lista gracias al emulador del paso 1.

Paso 3: Preparar una aplicación y un Dockerfile multiplataforma

Crea un directorio para el proyecto:

mkdir -p ~/hello-arch && cd ~/hello-arch

Crea una aplicación de Go que muestra la arquitectura en la que se ejecuta:

nano main.go
package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Printf("Hola desde %s/%s\n", runtime.GOOS, runtime.GOARCH)
}

Crea el Dockerfile:

nano Dockerfile
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY main.go .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/hello main.go

FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=build /out/hello /usr/local/bin/hello
ENTRYPOINT ["hello"]

Las piezas clave son las variables que BuildKit define automáticamente en cada build:

  • BUILDPLATFORM es la plataforma de la máquina que construye (aquí linux/amd64). Con FROM --platform=$BUILDPLATFORM, la etapa de compilación se ejecuta siempre de forma nativa, sin emulación.
  • TARGETOS y TARGETARCH indican la plataforma de destino de cada variante (linux y amd64 o arm64). Go las usa para compilar el binario de la arquitectura correcta.
  • La etapa final FROM alpine:3.20 no lleva --platform, así que Buildx usa la variante de Alpine de cada plataforma de destino. Su RUN apk add es lo único que se ejecuta con QEMU en la variante arm64, y es un paso rápido.

Paso 4: Construir y publicar la imagen

Inicia sesión en Docker Hub. Usa un token de acceso en lugar de tu contraseña:

docker login -u your_user

Construye las dos variantes y publícalas en una sola etiqueta:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t your_user/hello-arch:1.0 \
  --push .

Buildx construye ambas plataformas en paralelo y, al terminar, sube las imágenes y el índice que las agrupa:

[+] Building 38.2s (22/22) FINISHED                        docker-container:multiarch
 => [linux/amd64 build 5/5] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ...
 => [linux/arm64 build 5/5] RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ...
 ...
 => pushing manifest for docker.io/your_user/hello-arch:1.0@sha256:...

Observa que las dos etapas build aparecen con su plataforma de destino, pero ambas se ejecutan de forma nativa.

Paso 5: Verificar el índice y probar cada arquitectura

Inspecciona la etiqueta publicada para ver las variantes que contiene:

docker buildx imagetools inspect your_user/hello-arch:1.0
Name:      docker.io/your_user/hello-arch:1.0
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:4c1e...

Manifests:
  Name:        docker.io/your_user/hello-arch:1.0@sha256:9a2b...
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/amd64

  Name:        docker.io/your_user/hello-arch:1.0@sha256:e7f0...
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/arm64

  Name:        docker.io/your_user/hello-arch:1.0@sha256:1d3c...
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    unknown/unknown
  Annotations:
    vnd.docker.reference.type: attestation-manifest

Las entradas unknown/unknown son atestaciones de procedencia que BuildKit añade por defecto; no son imágenes ejecutables.

Ejecuta la imagen en la arquitectura nativa del servidor:

docker run --rm your_user/hello-arch:1.0
Hola desde linux/amd64

Y fuerza la variante arm64, que se ejecutará con QEMU:

docker run --rm --platform linux/arm64 your_user/hello-arch:1.0
Hola desde linux/arm64

En un servidor arm64 real, docker run your_user/hello-arch:1.0 descargará directamente la variante arm64 sin que tengas que indicar nada.

Paso 6: Acelerar builds con caché en el registro

El builder docker-container guarda su caché dentro de su propio contenedor, y se pierde si lo eliminas o si construyes desde otra máquina, como un runner de CI. Puedes guardar la caché en el registro junto a la imagen:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t your_user/hello-arch:1.1 \
  --cache-from type=registry,ref=your_user/hello-arch:buildcache \
  --cache-to type=registry,ref=your_user/hello-arch:buildcache,mode=max \
  --push .

mode=max guarda también las capas de las etapas intermedias, como la de compilación, que son las que más tiempo ahorran. Si repites el comando sin cambiar el código, verás CACHED en casi todos los pasos.

Paso 7: Probar localmente una sola plataforma

Con el almacén de imágenes clásico de Docker no puedes cargar un índice multiplataforma en local con --load, pero sí una sola plataforma. Es útil para probar sin publicar:

docker buildx build --platform linux/arm64 -t hello-arch:arm64-test --load .
docker run --rm hello-arch:arm64-test
Hola desde linux/arm64

Solución de problemas

ERROR: Multi-platform build is not supported for the docker driver. Estás usando el builder por defecto. Activa el builder del paso 2 con docker buildx use multiarch.

exec format error al construir o ejecutar. El emulador de QEMU no está registrado, habitual después de reiniciar el servidor si usaste solo tonistiigi/binfmt. Repite el paso 1 o instala qemu-user-static.

El build de arm64 es muy lento. Alguna instrucción pesada (compilar, npm install con módulos nativos) se ejecuta con emulación. Muévela a una etapa con FROM --platform=$BUILDPLATFORM y usa compilación cruzada, o añade un nodo arm64 nativo al builder con docker buildx create --append --name multiarch ssh://usuario@servidor-arm64.

denied: requested access to the resource is denied al hacer push. No has iniciado sesión o la etiqueta no empieza por tu usuario u organización del registro.

Conclusión

Has registrado QEMU, creado un builder de Buildx y publicado una imagen para amd64 y arm64 bajo una sola etiqueta, usando compilación cruzada para que el paso pesado no dependa de la emulación, y has añadido caché en el registro. Como siguientes pasos, puedes automatizar el build en CI con las acciones oficiales docker/setup-qemu-action, docker/setup-buildx-action y docker/build-push-action, añadir un nodo ARM nativo al builder o publicar las imágenes en un registro privado.