Guía10 min

Buildx multi-platform para un agente IA: amd64 y arm64 en un tag

Resumen

Cómo publicar un agente que corre en Mac ARM y en VPS x86 con un solo tag: docker buildx --platform linux/amd64,linux/arm64, driver docker-container, QEMU vs nodos nativos vs cross-compile, y GitHub Actions con setup-qemu + setup-buildx. Distinto de Bake, de cache y de pin digest.

DockerGitHub
Un tag de imagen con dos manifiestos: linux/amd64 y linux/arm64 para el mismo agente

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

Un docker build en tu Mac M-series produce linux/arm64. El VPS de self-hosting es linux/amd64. Sin un manifest list, docker pull en el VPS falla o cae a emulación lenta. Multi-platform no es “compilar dos veces a mano”: es un tag que apunta a dos manifiestos y el daemon elige el de la máquina.

No es Bake (eso declara targets). No es cache de capas (eso acelera el mismo Dockerfile). No es pin de digest (eso fija qué SHA tiras; aquí fijas para qué CPU). Hub: seguridad, coste y operación.

La regla

Un tag, dos (o más) plataformas. El registry guarda el índice más cada variante. El cliente pide ghcr.io/org/agente:1.4 y recibe linux/arm64 en el Mac y linux/amd64 en el VPS. Si publicas solo la arquitectura del laptop, el agente “funciona en mi máquina” y explota en Fly/Railway/Hetzner.

El comando mínimo, con Buildx:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag ghcr.io/org/agente:1.4 \
  --push .

--push no es opcional si el driver es docker-container: esa imagen no se carga sola en el store clásico. Docker Desktop y Engine 29+ con containerd image store sí pueden cargar el índice; el runner default de GitHub Actions no.

Tres estrategias (elige una)

Docker documenta tres vías. Para un agente Node/Python con pnpm --prod o pip, no son intercambiables.

EstrategiaCuándoCosto
QEMU (emulación)Dockerfile sin cambios; Desktop ya trae QEMUFácil. Lento en npm ci, compiladores nativos, Playwright
Nodos nativosUn builder con un nodo amd64 y otro arm64 (o Docker Build Cloud)Rápido. Overhead de cluster
Cross-compileGo/Rust/Zig con BUILDPLATFORM / TARGETPLATFORMEl FROM --platform=$BUILDPLATFORM evita QEMU en el stage de build

QEMU se instala a mano solo si el BuildKit no trae emuladores (paquete de terceros). El one-liner oficial es docker run --privileged --rm tonistiigi/binfmt --install all. Verifica flags F en /proc/sys/fs/binfmt_misc/qemu-*. En Desktop no hace falta.

Cross-compile (docs oficiales, 7 sep 2026):

# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM node:22-alpine AS deps
ARG TARGETPLATFORM
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile --prod

$BUILDPLATFORM es la CPU del builder; $TARGETPLATFORM es linux/arm64 o linux/amd64. Si el RUN ejecuta binarios del target (un node-gyp nativo), esto no sustituye QEMU: o compilas cruzado de verdad, o dejas que emule.

Índice de imagen con dos manifiestos, uno por arquitectura

Builder: no uses el default a ciegas

El builder docker (el de docker build clásico) no produce índices multi-arch de forma fiable en Engine viejo. Crea uno con driver docker-container:

docker buildx create \
  --name container-builder \
  --driver docker-container \
  --bootstrap --use

Ese driver no carga el resultado al docker images local. Eso no es un bug: es el contrato. Para probar en el laptop usa --load con una sola --platform (la tuya). Para publicar, --push al registry.

Nodos nativos, si ya tienes contextos node-amd64 y node-arm64:

docker buildx create --use --name mybuild node-amd64
docker buildx create --append --name mybuild node-arm64
docker buildx build --platform linux/amd64,linux/arm64 --push .

No improvises un segundo nodo en la misma Mac con QEMU y lo llames “nativo”. Nativo = hardware de esa arch.

GitHub Actions: el patrón que no inventes

Docs de Docker para GHA (consultadas el 7 de septiembre de 2026). Acciones actuales: docker/setup-qemu-action@v4, docker/setup-buildx-action@v4, docker/build-push-action@v7, docker/login-action@v4.

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: docker/login-action@v4
        with:
          username: ${{ vars.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - uses: docker/setup-qemu-action@v4
      - uses: docker/setup-buildx-action@v4
      - uses: docker/build-push-action@v7
        with:
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ghcr.io/org/agente:1.4

Tres trampas:

  1. load: true + dos plataformas en el runner default falla. El store local no traga el índice. Si el job necesita la imagen para un test, o habilitas containerd snapshotter con docker/setup-docker-action, o construyes una plataforma (linux/amd64 en ubuntu-latest) para el test y el job de publish hace el índice.
  2. QEMU en npm ci / Playwright / Chromium infla el job 3–10×. Si el Dockerfile instala browsers, parte el matrix: un runner ubuntu-latest (amd64) y otro ubuntu-24.04-arm (arm64) y luego un merge. Docker documenta docker/github-builder para no inventar el job de manifiesto.
  3. user/app:latest sin digest. El índice también se pineá: publica el tag y guarda el digest del índice (no el de una sola arch). El pin vive en la guía de digest.

Pipeline de Actions: QEMU, Buildx y push del índice amd64+arm64

Agente concreto: qué construir

Un bot de Telegram en Node que corre en Fly (amd64) y en un Mac Mini de staging (arm64) necesita las mismas capas de runtime, no un Dockerfile “si Darwin”. Receta:

  1. Multi-stage: stage deps + stage runner con USER node.
  2. --platform linux/amd64,linux/arm64 solo en CI. Local: docker buildx build --platform linux/arm64 --load (o amd64, la tuya).
  3. Healthcheck con node -e, no wget (Alpine no lo trae).
  4. No copies node_modules del host: el lockfile sí, el install corre dentro de cada plataforma.

Si el agente usa Chromium, QEMU duele. Ahí gana el matrix nativo o un binario de Playwright por arch (npx playwright install --with-deps en cada runner), no un FROM mcr.microsoft.com/playwright emulado.

Checklist

  • docker buildx ls muestra un builder docker-container (o Desktop 29+ con containerd).
  • --platform linux/amd64,linux/arm64 y --push al registry; no --load del índice en el runner default.
  • Local prueba una arch con --load.
  • CI: setup-qemu-action@v4 + setup-buildx-action@v4 + build-push-action@v7.
  • Jobs pesados (Playwright, compiladores) no pasan por QEMU: matrix nativo o cross-compile real.
  • El tag publicado se verifica con docker buildx imagetools inspect ghcr.io/org/agente:1.4 (deben listarse ambas plataformas).
  • El deploy (Fly/Railway/Compose) pineá digest del índice, no un SHA de una sola arch.

FAQ

¿docker build --platform linux/amd64 en el Mac basta? Produce una imagen amd64 vía QEMU. El tag no es multi-arch. El VPS amd64 corre; el Mini ARM no, o al revés.

¿Puedo hacer dos tags :amd64 y :arm64? Sí, y es peor UX. El índice es el contrato. Dos tags obligan a que Compose/Fly elijan a mano.

¿Bake reemplaza esto? No. Bake puede pasar platforms = ["linux/amd64","linux/arm64"] por target. El índice lo sigue haciendo Buildx.

¿Por qué imagetools inspect muestra un manifest y no dos? Construiste una sola plataforma, o pusheaste con el driver docker clásico. Recrea el builder docker-container y vuelve a --push.

Fuentes oficiales Docker y actions setup-qemu / setup-buildx verificadas el 7 de septiembre de 2026.