Docker buildx multi-arch: deja de publicar binarios equivocados

¿Te fue útil?

«Error de formato de ejecución.» El mensaje más honesto que puede dar tu contenedor. Es el equivalente en tiempo de ejecución a mirar una puerta marcada con empujar y tirar de ella de todos modos: publicaste el binario para la CPU equivocada.

Multi-arch debería hacer esto aburrido: publica una etiqueta y funciona en todas partes. En pipelines de producción reales, es fácil publicar accidentalmente una imagen solo amd64 bajo una etiqueta que tus nodos arm64 descargarán encantados. O peor: publicar una lista de manifiestos que afirma que existe arm64 mientras el contenido de las capas es en secreto amd64. Ahí es cuando te llaman a las 02:17 por «Kubernetes está roto otra vez». No lo está. Tu build lo está.

Qué es realmente “multi-arch” (y qué no es)

Multi-arch en Docker no es magia. Es un truco de empaquetado: una única etiqueta de imagen apunta a una lista de manifiestos (también llamada “index”) que contiene un manifiesto de imagen por plataforma. El registro sirve el correcto según la plataforma solicitada por el cliente.

Eso es todo. El registro no valida tus binarios. Docker no lee tus encabezados ELF y dice “hmm, sospechoso”. Si empujas un manifiesto arm64 que referencia una capa amd64, publicarás una mentira con éxito. El runtime solo se quejará cuando el kernel se niegue a ejecutarlo.

Términos clave que debes dejar de pasar por alto

  • Plataforma: OS + arquitectura (+ variante opcional). Ejemplo: linux/amd64, linux/arm64, linux/arm/v7.
  • Manifiesto: JSON que describe una imagen específica para una plataforma: config + capas.
  • Lista de manifiestos / index: JSON que mapea plataformas a manifiestos. Esto es lo que quieres que sea tu etiqueta cuando dices “multi-arch”.
  • BuildKit: El motor de compilación detrás de docker buildx. Hace el trabajo pesado: compilación cruzada, caché, exportación y builders remotos.
  • Emulación QEMU: Una forma de ejecutar binarios no nativos durante la compilación. Es conveniente, más lenta y a veces sutilmente defectuosa.

Si interiorizas una cosa: multi-arch trata de publicar metadatos correctos y bytes correctos. Necesitas ambos.

Datos rápidos e historia que realmente importan

  1. Las “listas de manifiestos” de Docker aparecieron años después de que las imágenes Docker se hicieran populares. Docker temprano no tenía una historia multi-arch de primera clase; la gente usaba etiquetas separadas como :arm y :amd64 y rezaba para no equivocarse al teclear.
  2. Las especificaciones de imagen OCI estandarizaron lo que almacenan los registros. La mayoría de los registros modernos almacenan manifiestos compatibles con OCI incluso si los llamas “imágenes Docker”. Las formas JSON están bien definidas; lo que varía es tu tooling.
  3. BuildKit fue un cambio arquitectónico importante. El clásico docker build estaba atado al daemon y no fue diseñado para compilaciones cruzadas a escala. BuildKit hizo las compilaciones más paralelas, cacheables y exportables.
  4. El auge de arm64 en servidores cambió el modelo de amenazas. Antes era hobby con placas; ahora son instancias de nube mainstream. Descargar una imagen solo amd64 en un nodo arm64 ya no es “raro”.
  5. Kubernetes multi-arch no es especial. Depende de la misma negociación de manifiestos del registro que Docker. Si tu etiqueta está mal, Kubernetes descargará la cosa equivocada más rápido de lo que puedes decir “rollout restart”.
  6. Las diferencias entre musl de Alpine y glibc de Debian golpean más bajo bajo emulación. QEMU puede enmascarar incompatibilidades de ABI hasta el tiempo de ejecución, y entonces obtienes fallos que parecen bugs de la aplicación.
  7. La “variante” importa para algunos objetivos ARM. linux/arm/v7 vs linux/arm/v6 no es trivial. Si publicas la variante equivocada, el binario puede ejecutarse y aun así fallar de formas extrañas.
  8. Los registros generalmente no validan la corrección de la plataforma. Almacenan lo que empujas. Trata al registro como almacenamiento duradero, no como una puerta de calidad.

Multi-arch está lo suficientemente maduro como para ser aburrido—si lo construyes como un SRE que ya se quemó antes.

Cómo terminas publicando los binarios equivocados

1) Has empujado una imagen de una sola arquitectura bajo una etiqueta “universal”

Alguien ejecutó docker build -t myapp:latest . en un portátil amd64 y la subió. Tus nodos arm64 descargan :latest, obtienen capas amd64 y fallan con exec format error.

Este es el clásico. Sigue pasando en 2026 porque la gente sigue teniendo dedos.

2) Compilaste multi-arch, pero solo una plataforma realmente tuvo éxito

Buildx puede compilar múltiples plataformas en un solo comando. También puede producir silenciosamente resultados parciales si no eres estricto con los fallos en CI y no verificas la lista de manifiestos después.

3) Tu Dockerfile “amistosamente” descarga el binario precompilado equivocado

La forma más común de envío de arquitectura equivocada no es la compilación. Es el curl. Un Dockerfile que hace:

  • curl -L -o tool.tgz ...linux-amd64... independientemente de la plataforma
  • o usa un script instalador que por defecto elige amd64
  • o asume que uname -m dentro del contenedor de compilación equivale a la arquitectura objetivo

Bajo emulación, uname -m puede reportar lo que esperas, hasta que no. Bajo compilación cruzada, puede reportar la arquitectura del entorno de build, no la del objetivo.

4) Confiabas en QEMU para builds “que funcionarán” y obtuviste “funciona a veces”

QEMU es genial para avanzar. No es genial para fingir que es idéntico a una ejecución nativa. Algunos ecosistemas de lenguajes (no doy nombres, pero sí, ese) ejecutan detección de arquitectura durante la compilación. Bajo emulación, la detección puede fallar, ser lenta o inestable.

5) Tu caché mezcló arquitecturas y no lo notaste

Los cachés de build son direccionados por contenido, pero tu lógica de compilación aún puede producir contaminación cruzada si escribes en rutas compartidas, reutilizas artefactos entre fases o traes “latest” sin fijar activos por arquitectura.

6) Tus runners de CI son multi-arch y tus suposiciones son single-arch

Cuando tu flota incluye runners amd64 y arm64, “compilar en lo que esté disponible” se convierte en “publicar lo que se compiló”. Multi-arch requiere explicitud: plataformas, procedencia y verificación.

Una frase que vale la pena mantener en la cabeza mientras haces este trabajo: La esperanza no es una estrategia. (idea parafraseada, frecuentemente atribuida a ingenieros y operadores en círculos de fiabilidad)

Broma #1: Las imágenes multi-arch son como adaptadores de corriente—todo parece compatible hasta que realmente lo enchufas.

Guía rápida de diagnóstico

Cuando algo falla en prod, no obtienes puntos por una teoría elegante. Obtienes puntos por restaurar el servicio y prevenir la repetición. Aquí está la ruta más rápida que conozco.

Primero: prueba qué se descargó

  1. Comprueba la arquitectura del nodo. Si el nodo es arm64 y la imagen es solo amd64, para ahí.
  2. Inspecciona la lista de manifiestos de la etiqueta. ¿Incluye la etiqueta la plataforma que crees?
  3. Resuelve el digest que realmente se descargó. Las etiquetas se mueven. Los digests no. Encuentra el digest que el runtime está usando.

Segundo: valida los bytes dentro de la imagen

  1. Arranca un contenedor de depuración e inspecciona un binario con file. Si dice x86-64 en un objetivo arm64, encontraste la pistola humeante.
  2. Revisa el enlazador dinámico / expectativas de libc. La arquitectura equivocada es obvia. El ABI equivocado puede ser más sigiloso.

Tercero: rastrea la fuente de la build

  1. Mira el Dockerfile buscando descargas. Cualquier cosa que obtenga artefactos precompilados debe ser consciente de la plataforma.
  2. Revisa los logs de buildx por pasos por plataforma. Un badge de job en verde puede ocultar una build parcial.
  3. Verifica la configuración de QEMU/binfmt si hay emulación involucrada. Si tu build depende de emulación, trátala como una dependencia con chequeos de salud.

El cuello de botella más común

No es BuildKit. Es la falta de verificación en tu pipeline. La build “funcionó” y publicó una etiqueta rota. El sistema hizo exactamente lo que le dijiste, y ese es el problema.

Tareas prácticas: comandos, salidas, decisiones

Estos no son comandos “de juguete”. Son lo que ejecutas durante un incidente y lo que automatizas después para que no tengas incidentes.

Tarea 1: Confirma la arquitectura de tu host (no supongas)

cr0x@server:~$ uname -m
aarch64

Qué significa: El nodo es ARM 64-bit. Espera imágenes linux/arm64.

Decisión: Si la etiqueta de la imagen no anuncia linux/arm64, ya estás en territorio de “binario equivocado”.

Tarea 2: Ve qué plataformas declara tu etiqueta

cr0x@server:~$ docker buildx imagetools inspect myorg/myapp:latest
Name:      myorg/myapp:latest
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:8c9c2f7b4f8a5b0d5c0a2b1e9c3d1a6e2f4b7a9c0d1e2f3a4b5c6d7e8f9a0b1c

Manifests:
  Name:      myorg/myapp:latest@sha256:111...
  Platform:  linux/amd64
  Name:      myorg/myapp:latest@sha256:222...
  Platform:  linux/arm64

Qué significa: La etiqueta es un índice multi-arch que contiene variantes amd64 y arm64.

Decisión: Si tu plataforma falta aquí, arregla la publicación primero. Si está presente, sigue a verificar el contenido de la imagen arm64.

Tarea 3: Inspecciona la lista de manifiestos con la CLI de Docker (vista alternativa)

cr0x@server:~$ docker manifest inspect myorg/myapp:latest | head -n 20
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.oci.image.index.v1+json",
   "manifests": [
      {
         "mediaType": "application/vnd.oci.image.manifest.v1+json",
         "digest": "sha256:111...",
         "platform": {
            "architecture": "amd64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.oci.image.manifest.v1+json",
         "digest": "sha256:222...",

Qué significa: Estás viendo el mapeo bruto de plataformas.

Decisión: Al depurar rarezas del registro, el JSON no miente. Si la lista de manifiestos no incluye tu plataforma, deja de culpar a Kubernetes.

Tarea 4: Fuerza la descarga de una plataforma específica localmente

cr0x@server:~$ docker pull --platform=linux/arm64 myorg/myapp:latest
latest: Pulling from myorg/myapp
Digest: sha256:222...
Status: Downloaded newer image for myorg/myapp:latest

Qué significa: Pediste la variante arm64 y obtuviste un digest específico.

Decisión: Usa ese digest para inspección profunda. Si la descarga falla con “no matching manifest”, tu publicación multi-arch está incompleta.

Tarea 5: Confirma la metadata de arquitectura en la config de la imagen

cr0x@server:~$ docker image inspect myorg/myapp:latest --format '{{.Architecture}} {{.Os}}'
arm64 linux

Qué significa: La metadata local de la imagen declara que es arm64.

Decisión: Buena señal, no prueba. Ahora verifica los ejecutables reales.

Tarea 6: Comprueba la arquitectura del binario dentro del contenedor

cr0x@server:~$ docker run --rm --entrypoint /bin/sh myorg/myapp:latest -c 'file /usr/local/bin/myapp'
/usr/local/bin/myapp: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, stripped

Qué significa: Los bytes son realmente arm64.

Decisión: Si dice “x86-64”, tienes un paso de build que produjo el binario equivocado (habitualmente lógica de descarga). Arregla el Dockerfile, no el registro.

Tarea 7: Reproduce de forma segura la firma del fallo (“exec format error”)

cr0x@server:~$ docker run --rm --platform=linux/arm64 myorg/myapp:latest /usr/local/bin/myapp --version
myapp 2.7.1

Qué significa: La variante arm64 se ejecuta y muestra la versión.

Decisión: Si obtienes exec format error, el runtime está intentando ejecutar la arquitectura equivocada (o el archivo no es ejecutable para ese OS/arch). Vuelve a la Tarea 6.

Tarea 8: Verifica qué builder estás usando (y si es sensato)

cr0x@server:~$ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT             STATUS    BUILDKIT   PLATFORMS
default         docker
  default       unix:///var/run/docker.sock running   v0.12.5    linux/amd64
multiarch       docker-container
  multiarch0    unix:///var/run/docker.sock running   v0.12.5    linux/amd64,linux/arm64,linux/arm/v7

Qué significa: El builder por defecto solo puede hacer amd64; el builder docker-container soporta múltiples plataformas (probablemente vía binfmt/QEMU).

Decisión: Usa una instancia de builder dedicada para multi-arch. No confíes en “default” salvo que te gusten las sorpresas.

Tarea 9: Crea y selecciona un builder multi-arch apropiado

cr0x@server:~$ docker buildx create --name multiarch --driver docker-container --use
multiarch

Qué significa: Buildx ejecutará un contenedor BuildKit para comportamiento consistente y características multi-plataforma.

Decisión: En CI, siempre crea/usa un builder nombrado. Hace las builds reproducibles y depurables.

Tarea 10: Comprueba el registro binfmt/QEMU en el host

cr0x@server:~$ docker run --privileged --rm tonistiigi/binfmt --info
Supported platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v6
Enabled platforms: linux/amd64, linux/arm64, linux/arm/v7

Qué significa: El kernel tiene manejadores binfmt habilitados para esas arquitecturas.

Decisión: Si tu plataforma objetivo no está habilitada, las builds multi-arch que requieran emulación fallarán o ignorarán pasos silenciosamente. Habilita la plataforma o cambia a builders nativos por arquitectura.

Tarea 11: Compilar y empujar una imagen multi-arch (la forma correcta)

cr0x@server:~$ docker buildx build --platform=linux/amd64,linux/arm64 -t myorg/myapp:2.7.1 --push .
[+] Building 142.6s (24/24) FINISHED
 => [internal] load build definition from Dockerfile                                  0.0s
 => => transferring dockerfile: 2.12kB                                                0.0s
 => [linux/amd64] exporting to image                                                  8.1s
 => => pushing layers                                                                 6.7s
 => [linux/arm64] exporting to image                                                  9.4s
 => => pushing layers                                                                 7.5s
 => exporting manifest list                                                           1.2s
 => => pushing manifest list                                                          0.6s

Qué significa: Compilaste ambas plataformas y subiste una lista de manifiestos.

Decisión: Si no ves “exporting manifest list”, probablemente no empujaste un índice multi-arch. Arregla eso antes de declarar la victoria.

Tarea 12: Valida que la etiqueta empujada es un índice, no un manifiesto único

cr0x@server:~$ docker buildx imagetools inspect myorg/myapp:2.7.1 | sed -n '1,20p'
Name:      myorg/myapp:2.7.1
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:4aa...

Manifests:
  Name:      myorg/myapp:2.7.1@sha256:aaa...
  Platform:  linux/amd64
  Name:      myorg/myapp:2.7.1@sha256:bbb...
  Platform:  linux/arm64

Qué significa: El registro ahora almacena un índice multi-arch verdadero para esa etiqueta.

Decisión: Bloquea tu pipeline con esta comprobación. Si no es un índice, falla la build.

Tarea 13: Diagnostica un Dockerfile que descarga el activo equivocado

cr0x@server:~$ docker buildx build --platform=linux/arm64 --progress=plain --no-cache .
#10 [linux/arm64 6/9] RUN curl -fsSL -o /usr/local/bin/helm.tgz "https://example/helm-linux-amd64.tgz"
#10 0.9s curl: (22) The requested URL returned error: 404

Qué significa: Tu Dockerfile codificó amd64 (y está fallando en arm64, lo cual es misericordioso). A menudo no devolverá 404 y “funcionará” mientras publica los bytes equivocados.

Decisión: Reemplaza la arquitectura codificada por los args de BuildKit (TARGETARCH, TARGETOS, TARGETVARIANT) y mapea esos valores al nombrado de upstream.

Tarea 14: Usa correctamente los argumentos de plataforma que provee BuildKit

cr0x@server:~$ docker buildx build --platform=linux/arm64 --progress=plain --no-cache -t test/myapp:arm64 .
#5 [linux/arm64 3/7] RUN echo "TARGETOS=$TARGETOS TARGETARCH=$TARGETARCH TARGETVARIANT=$TARGETVARIANT"
#5 0.1s TARGETOS=linux TARGETARCH=arm64 TARGETVARIANT=

Qué significa: BuildKit inyecta valores de plataforma objetivo; debes basar descargas/compilación en estos, no en uname.

Decisión: Si tu Dockerfile usa uname -m para elegir binarios, estás a una refactorización de dolor. Cambia a args de BuildKit.

Tarea 15: Confirma la plataforma del contenedor en Kubernetes (desde un nodo)

cr0x@server:~$ kubectl get node -o wide | head -n 3
NAME           STATUS   ROLES    AGE   VERSION   INTERNAL-IP   OS-IMAGE             KERNEL-VERSION
worker-arm01   Ready    <none>   55d   v1.29.1   10.0.3.21     Ubuntu 22.04.3 LTS    5.15.0-91-generic
worker-x86a01  Ready    <none>   55d   v1.29.1   10.0.2.14     Ubuntu 22.04.3 LTS    5.15.0-91-generic

Qué significa: Tienes un cluster de arquitectura mixta (ahora común). El scheduling colocará pods en cualquiera a menos que estén limitados.

Decisión: Si la imagen no es realmente multi-arch, debes fijar selectores/afinidad de nodo temporalmente o experimentarás la ruleta de “funciona en un nodo”.

Tarea 16: Identifica el digest de imagen que usa un pod

cr0x@server:~$ kubectl get pod myapp-5f7f6d9c7b-2qk4p -o jsonpath='{.status.containerStatuses[0].imageID}{"\n"}'
docker-pullable://myorg/myapp@sha256:bbb...

Qué significa: El pod resolvió la etiqueta a un digest (inmutable). Ese digest corresponde a un manifiesto de plataforma.

Decisión: Compara este digest con tu lista de manifiestos. Si apunta a la entrada de plataforma equivocada, tu metadata del registro o la negociación de plataforma del cliente están mal.

Tarea 17: Depura el comportamiento del caché de BuildKit (y para de “optimizar” a ciegas)

cr0x@server:~$ docker buildx build --platform=linux/amd64,linux/arm64 \
  --cache-to=type=registry,ref=myorg/myapp:buildcache,mode=max \
  --cache-from=type=registry,ref=myorg/myapp:buildcache \
  -t myorg/myapp:ci-test --push .
[+] Building 98.3s (24/24) FINISHED
 => importing cache manifest from myorg/myapp:buildcache                               2.4s
 => [linux/amd64] CACHED                                                               0.6s
 => [linux/arm64] CACHED                                                               0.8s
 => exporting manifest list                                                            1.1s

Qué significa: El caché se reutilizó para ambas plataformas y se exportó una lista de manifiestos.

Decisión: Si ves una plataforma en caché y la otra reconstruyéndose desde cero cada vez, el orden del Dockerfile o los pasos condicionales por plataforma están rompiendo la reutilización del caché.

Tarea 18: Detecta “una sola arquitectura haciéndose pasar por multi-arch” en CI

cr0x@server:~$ docker buildx imagetools inspect myorg/myapp:ci-test | grep -E 'MediaType|Platform'
MediaType: application/vnd.oci.image.index.v1+json
  Platform:  linux/amd64
  Platform:  linux/arm64

Qué significa: Tu etiqueta es un índice e incluye ambas plataformas.

Decisión: Haz de esto un paso obligatorio del pipeline. Si imprime application/vnd.oci.image.manifest.v1+json en su lugar, falla el job.

Una estrategia de compilación que se comporte en CI

La estrategia correcta depende de lo que compiles. Los lenguajes compilados se comportan diferente a builds que “hacen curl de un binario en /usr/local/bin”. Pero los principios son consistentes:

Principio 1: Prefiere builds nativos por arquitectura cuando puedas

Si tienes la opción de ejecutar un runner arm64 y un runner amd64, aprovéchala. Los builds nativos evitan sorpresas de emulación y suelen ser más rápidos para compilaciones pesadas.

Eso no significa que necesites dos pipelines totalmente separados. Significa que deberías considerar una topología de builders:

  • Builders remotos por arquitectura (BuildKit soporta este patrón)
  • O jobs de CI separados que empujen imágenes por-arch y luego publiquen una lista de manifiestos

Principio 2: Si usas QEMU, trátalo como infraestructura de producción

QEMU vía binfmt no es “solo una comodidad para desarrolladores” una vez que está en CI. Puede romperse con actualizaciones de kernel, Docker y hasta endurecimientos de seguridad. Monitorízalo y valídalo.

Principio 3: Haz el Dockerfile consciente de la plataforma sin complicarte

Usa los args de BuildKit. Existen por una razón:

  • TARGETOS, TARGETARCH, TARGETVARIANT
  • BUILDOS, BUILDARCH para el entorno de build

Luego mapea al nombrado del proveedor explícitamente. Muchos upstreams usan x86_64 en lugar de amd64. O aarch64 en lugar de arm64. No adivines; mapea.

Principio 4: Separa “descargar herramientas” de “compilar la app”

Cuanto más mezcles responsabilidades, más riesgoas contaminación de caché y mezclas de plataforma. Un patrón limpio:

  • Etapa A: obtener o compilar herramientas específicas de plataforma (claveada por target arch)
  • Etapa B: compilar tu aplicación (cross-compile si corresponde)
  • Etapa C: imagen de runtime mínima (copiar exactamente lo que necesitas)

Principio 5: Siempre verifica la etiqueta publicada

La verificación es barata. Los incidentes son caros. Después de empujar, inspecciona la lista de manifiestos, descarga cada variante de plataforma y verifica el binario principal con file. Automatízalo.

Broma #2: Si no verificas multi-arch, básicamente ejecutas una lotería multiplataforma cuyo premio es una caída del servicio.

Tres micro-historias corporativas (anonimizadas, dolorosamente familiares)

Micro-historia 1: El incidente causado por una suposición errónea

Una compañía mediana migró una parte de sus nodos Kubernetes a arm64 para reducir costes. El equipo de plataforma hizo lo razonable: marcaron los nuevos nodos, movieron servicios de bajo riesgo primero y observaron las tasas de error. Pareció estar bien durante una semana.

Luego un redeploy rutinario de una API interna “aburrida” empezó a fallar solo en los nodos arm. On-call vio crash loops con exec format error. Asumieron que era un problema de la imagen base y revertieron la actualización del cluster que acababan de hacer. Nada cambió. Revirtieron la aplicación. Sigue roto en arm.

La suposición errónea fue simple: “Usamos buildx, por lo tanto nuestras imágenes son multi-arch.” Sí usaban buildx. Pero el pipeline publicó una etiqueta desde un único job de docker build que corría en un runner amd64. Otro job compiló arm64 para pruebas, pero nunca la subió.

La solución fue igualmente simple: hacer que publicar una lista de manifiestos sea el único camino hacia :latest y etiquetas de release, y bloquear el pipeline con imagetools inspect más una comprobación binaria. La lección cultural más profunda fue: ninguna etiqueta debería existir sin un paso de verificación, incluso si el equipo “sabe” cómo funciona.

Micro-historia 2: La optimización que resultó contraproducente

Una organización distinta tenía una build lenta y decidió “hacerla más rápida” cachéando agresivamente y reutilizando un directorio de artefactos compartido entre builds. Montaron un volumen de caché en el builder y almacenaron salidas compiladas indexadas solo por SHA de commit. Redujo el tiempo dramáticamente—por un tiempo.

Luego las imágenes arm64 empezaron a fallar con segmentation faults en una librería criptográfica durante handshakes TLS. No inmediatamente. Solo bajo carga. Las imágenes x86 estaban bien. Todos sospecharon un bug del compilador, luego una regresión del kernel, luego rayos cósmicos.

La causa raíz fue mundana e irritante: el directorio de caché compartido contenía objetos compilados para amd64 que se copiaban en la etapa de build arm64 porque los scripts usaban lógica de “si el archivo existe, reutilízalo”. Bajo QEMU, algunos pasos de build se saltaron y la caché evitó compilaciones en el peor momento. La metadata de la imagen todavía declaraba arm64, pero los bytes estaban mezclados.

Lo arreglaron haciendo que los cachés fueran con alcance por plataforma y negándose a compartir directorios de artefactos opacos entre plataformas. El caché en registro de BuildKit resolvió el problema original sin el atajo peligroso. La “optimización” había creado un problema de cadena de suministro cross-arch dentro del propio pipeline.

Micro-historia 3: La práctica aburrida pero correcta que salvó el día

Una compañía del sector financiero tenía control de cambios estricto y un proceso de release algo molesto. Los ingenieros se quejaban de los pasos extra: publicar imágenes por digest, registrar manifiestos y mantener un pequeño “registro de evidencia de release” por cada despliegue. Parecía papeleo. También hizo que sus on-call durmieran más tranquilos.

Durante una semana ocupada, un servicio empezó a caer solo en un subconjunto de nodos tras una reconstrucción rutinaria de la imagen. Esta vez la respuesta al incidente fue casi aburrida. Cogieron el digest imageID del pod, lo emparejaron con la lista de manifiestos y vieron inmediatamente que el digest arm64 referenciaba capas construidas dos días antes. El digest amd64 era nuevo.

El pipeline de build había fallado parcialmente al subir capas arm64 debido a un problema transitorio de permisos en el registro. El job aún publicó la etiqueta. Pero como el equipo siempre despliega por digest en producción y siempre registra el mapeo tag → digest → plataforma, pudieron fijar rápidamente el digest conocido-bueno para ambas plataformas y restaurar el servicio mientras arreglaban CI.

La práctica “aburrida”—promoción por digest y verificación de manifiestos—no evitó el error. Hizo pequeño el radio de impacto y rápida la diagnosis. Ese es el verdadero triunfo.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: exec format error al iniciar el contenedor

Causa raíz: Binario de arquitectura equivocada en la imagen, o la etiqueta apunta al manifiesto de plataforma incorrecto.

Solución: Inspecciona la etiqueta con docker buildx imagetools inspect. Descarga la plataforma intencionada con docker pull --platform=.... Confirma la arquitectura del binario con file. Arregla las descargas en el Dockerfile para usar TARGETARCH.

2) Síntoma: Funciona en nodos amd64, falla en nodos arm64, pero sin exec format error

Causa raíz: Componentes de userland de arquitecturas mixtas (plugins, libs compartidas) o incompatibilidad de ABI (musl vs glibc), a menudo introducido por scripts de “descargar último”.

Solución: Revisa la salida de file para cada binario copiado, no solo el principal. Fija imágenes base y versiones de herramientas. Prefiere paquetes de la distro o compilar desde fuente por arquitectura.

3) Síntoma: La etiqueta afirma soporte arm64, pero la descarga arm64 dice “no matching manifest”

Causa raíz: Empujaste un manifiesto de una sola arquitectura, no una lista de manifiestos; o sobrescribiste la etiqueta con un push single-arch.

Solución: Solo permite publicación de etiquetas desde docker buildx build --platform ... --push. Usa permisos de CI para prevenir pushes manuales a etiquetas de release.

4) Síntoma: Builds multi-arch dolorosamente lentas

Causa raíz: Emulación QEMU haciendo compilación pesada; sin caché; Dockerfile invalida caché con demasiada frecuencia.

Solución: Mueve a builders nativos por arquitectura o cross-compile cuando sea apropiado. Añade caché respaldada en registro. Reordena el Dockerfile para maximizar hits de caché.

5) Síntoma: build arm64 “success” pero en runtime no encuentra loader

Causa raíz: Copiaste un binario enlazado dinámicamente contra glibc dentro de una imagen musl (o viceversa), o copiaste solo el binario sin las libs compartidas requeridas.

Solución: Usa una familia de imágenes base consistente entre etapas, o compila estáticamente cuando proceda. Valida con ldd (si está disponible) o inspecciona con file y comprueba la ruta del intérprete.

6) Síntoma: CI muestra verde, pero prod descarga un digest más antiguo para una arquitectura

Causa raíz: Push parcial; lista de manifiestos actualizada incorrectamente; paso de caché o push falló para una plataforma mientras otro tuvo éxito.

Solución: Bloquea en verificación de manifiestos y requiere que ambas plataformas estén presentes y recién construidas. Considera empujar etiquetas por-arch primero y luego crear la lista de manifiestos explícitamente.

7) Síntoma: Docker Compose ejecuta la arquitectura equivocada localmente

Causa raíz: El entorno local por defecto usa una plataforma; Compose puede construir localmente para la arquitectura del host a menos que se establezca platform: o uses buildx correctamente.

Solución: Establece explícitamente platform en Compose para pruebas, o descarga con --platform. No tomes el comportamiento de Compose como prueba de que tu etiqueta de registro es correcta.

Listas de verificación / plan paso a paso

Paso a paso: endurecer un pipeline de release multi-arch

  1. Decide plataformas objetivo explícitamente. Para la mayoría de servicios backend: linux/amd64 y linux/arm64. Añade linux/arm/v7 solo si realmente lo soportas.
  2. Crea un builder nombrado en CI. Usa el driver docker-container para comportamiento consistente de BuildKit.
  3. Instala/verifica binfmt si dependes de emulación. Ejecuta el comando binfmt info y asegura que las plataformas objetivo estén habilitadas.
  4. Haz el Dockerfile consciente de la plataforma. Reemplaza lógica uname -m con mapeo por TARGETARCH.
  5. Compila y empuja multi-arch en una sola operación. Usa docker buildx build --platform=... --push.
  6. Verifica que la etiqueta publicada sea un índice. Falla el pipeline si el media type no es un OCI index.
  7. Verifica el binario principal de cada variante de plataforma. Descarga por plataforma y ejecuta file en el binario de entrada.
  8. Promociona por digest a producción. Despliega digests inmutables; usa etiquetas para humanos.
  9. Monitorea el sesgo por plataforma. En clusters mixtos, vigila crash loops correlacionados con la arquitectura del nodo.
  10. Restringe quién puede publicar etiquetas de release. Evita “arreglos rápidos” que eluden la verificación.

Lista de verificación: revisión del Dockerfile para corrección multi-arch

  • ¿Hay descargas con curl/wget? Si sí, ¿la URL varía según TARGETARCH?
  • ¿Hay scripts instaladores? ¿Soportan arm64 explícitamente o por defecto eligen amd64?
  • ¿Se copian artefactos compilados de otra etapa? ¿Las etapas están construidas para la misma --platform?
  • ¿Se usa uname o dpkg --print-architecture? ¿Estás seguro de que consulta la arquitectura objetivo y no el entorno de build?
  • ¿Fijas versiones y checksums por arquitectura? Si no, estás confiando en internet en estéreo.

Lista de verificación: puertas de verificación de release (seguridad mínima viable)

  • imagetools inspect muestra un OCI index e incluye todas las plataformas requeridas.
  • La descarga por plataforma tiene éxito.
  • El contenedor por plataforma ejecuta --version (o un comando ligero de salud).
  • La inspección binaria vía file coincide con la arquitectura esperada.

Preguntas frecuentes

1) ¿Necesito siempre QEMU para construir imágenes multi-arch?

No. Si tu build es puramente cross-compilación (por ejemplo, Go con las opciones adecuadas), puedes compilar para otras arquitecturas sin ejecutar binarios extranjeros. QEMU es necesario cuando pasos de build ejecutan binarios de la arquitectura objetivo durante la compilación.

2) ¿Cuál es la diferencia entre docker build y docker buildx build aquí?

buildx usa las características de BuildKit: salidas multi-plataforma, exportadores de caché avanzados y builders remotos. El clásico docker build suele ser single-platform y está ligado a la arquitectura del daemon local.

3) ¿Por qué la etiqueta muestra soporte arm64 pero el binario sigue siendo amd64?

Porque los manifiestos son metadata. Puedes publicar un manifiesto que diga “arm64” mientras la capa contiene un binario amd64. Esto suele ocurrir por descargas codificadas o contaminación del caché cross-arch.

4) ¿Debo compilar ambas arquitecturas en un mismo job o en jobs separados?

Si tienes runners nativos confiables para cada arquitectura, jobs separados pueden ser más rápidos y determinísticos. Si dependes de QEMU, una invocación única de buildx es más sencilla pero puede ser más lenta. De cualquier forma, publica una lista de manifiestos y verifícala.

5) ¿Puedo “arreglar” una etiqueta existente que está mal?

Puedes volver a empujar la etiqueta para apuntar a una lista de manifiestos corregida, pero caches y rollouts podrían ya haber descargado el digest roto. En producción, prefiere desplegar por digest para que la corrección sea una decisión de rollout explícita, no una sorpresa de etiqueta.

6) ¿Por qué Kubernetes a veces descarga la arquitectura equivocada?

Generalmente no lo hace. Descarga lo que la negociación de manifiestos provee para la plataforma del nodo. Cuando “descarga mal”, suele ser porque la etiqueta no era un índice multi-arch apropiado, o la entrada del índice referencia contenido incorrecto.

7) ¿Y linux/arm/v7—debería soportarlo?

Sólo si realmente tienes clientes en ARM de 32 bits. Soportarlo incrementa la complejidad de build, el tamaño de la matriz de pruebas y la probabilidad de errores de variante. No lo añadas como plataforma de adorno.

8) ¿Cómo hago que las “herramientas descargadas” sean seguras entre arquitecturas?

Usa mapeo basado en TARGETARCH, fija versiones y checksums por arquitectura. Mejor: usa paquetes de la distribución o compila desde fuente cuando sea práctico.

9) ¿Está bien usar :latest para multi-arch?

Está bien para conveniencia de desarrollador, no apropiado como contrato de producción. Usa digests inmutables o etiquetas versionadas para releases, y trata :latest como un puntero móvil.

10) ¿Cuál es la mejor salvaguarda única para prevenir binarios equivocados?

Un job de verificación post-push que (a) confirma que la etiqueta es un índice OCI con las plataformas requeridas, y (b) valida la arquitectura del binario de entrada con file para cada plataforma.

Conclusión: próximos pasos que puedes hacer esta semana

Si gestionas flotas de arquitectura mixta—o pronto lo harás—multi-arch no es un adorno opcional. Es higiene básica de releases. El registro almacenará gustosamente tus errores, y Kubernetes los desplegará a escala.

  1. Añade una puerta de verificación: después del push, exige que docker buildx imagetools inspect muestre un OCI index con todas las plataformas.
  2. Valida los bytes: para cada plataforma, descarga y ejecuta file en el ejecutable principal.
  3. Arregla las descargas en tu Dockerfile: reemplaza activos codificados para amd64 por mapeo con TARGETARCH.
  4. Elige una estrategia de builders: builders nativos por arquitectura si puedes; QEMU si debes—y luego monitorízalo como cualquier otra dependencia.
  5. Despliega por digest: haz rollouts explícitos, reversibles y depurables.

Multi-arch bien hecho es silencioso. Ese es el objetivo: sin heroísmos, sin caídas misteriosas, sin “funciona en mi nodo”. Solo binarios correctos, cada vez.

← Anterior
Actualizaciones sin tiempo de inactividad en Docker Compose: mito, realidad y patrones que funcionan
Siguiente →
Evolución de la VRAM: de GDDR simple a pura locura

Deja un comentario