Error ‘exec format’ de Docker: imágenes de arquitectura equivocada y la solución limpia

¿Te fue útil?

Despliegas un contenedor. Funcionó en tu portátil. Incluso funcionó en staging. Entonces producción dice: exec format error.
Los logs están vacíos, el pod se reinicia y el canal de incidentes se llena con la misma pregunta en diferentes tipografías: “¿Qué cambió?”

Normalmente: nada “cambió”. Simplemente pediste a una CPU que ejecute instrucciones hechas para otra CPU. Los contenedores no son magia.
Son empaquetado. Y el empaquetado sigue importando la arquitectura.

Qué significa realmente “exec format error”

exec format error es una queja a nivel de kernel. Linux intentó ejecutar un archivo y no pudo reconocerlo como un binario ejecutable
para la máquina actual. Esto no es Docker de mal humor. Es el SO anfitrión negándose a cargar el programa.

En el mundo de los contenedores, las causas más comunes son:

  • Arquitectura equivocada: tiraste una imagen arm64 en un nodo amd64, o al revés.
  • Formato binario incorrecto dentro de la imagen: tu base es amd64 pero copiaste un binario arm64 durante la compilación.
  • Shebang malo o CRLF en un script de entrypoint: el kernel no puede parsear la línea del intérprete o encuentra caracteres invisibles de Windows.
  • Loader dinámico ausente: compilaste para glibc pero desplegaste en un runtime Alpine (musl) sin el cargador esperado.

Pero el mensaje principal de error es el mismo, por eso los equipos desperdician horas discutiendo “Docker vs Kubernetes vs CI”
cuando el kernel ya te dijo el problema real: “No puedo ejecutar esto.”

Un modelo mental útil operativamente: una imagen de contenedor es un tarball lleno de archivos más algo de metadata. Cuando el contenedor arranca,
el kernel del host sigue ejecutando el entrypoint. Si ese entrypoint (o el intérprete al que apunta) no coincide con las expectativas de CPU
y ABI del host, el kernel devuelve ENOEXEC, y tu runtime lo convierte en una línea de log que mirarás entrecerrando los ojos durante un outage.

Guía rápida de diagnóstico

Cuando estás de guardia, no quieres una lección. Quieres una secuencia que converja rápido. Aquí está el orden que suele producir respuestas
en minutos, no horas.

1) Identifica la arquitectura del nodo (no asumas)

Primero verifica qué es realmente el host. “Es x86” suele ser folklore, y el folklore no es una señal de monitorización.

2) Identifica la arquitectura de la imagen descargada

Confirma la metadata local de la imagen: OS/Arch y si vino desde una manifest list.

3) Confirma qué archivo falla al ejecutarse

Encuentra el entrypoint y el comando, luego examina el tipo de ese archivo dentro de la imagen. Si es un script, revisa finales de línea y shebang.
Si es un binario, revisa los encabezados ELF y la arquitectura.

4) Decide: reconstruir vs seleccionar plataforma vs habilitar emulación

Jerarquía de arreglos en producción:

  1. Mejor: publicar una imagen multi-arch correcta y redeplegar.
  2. Solución temporal aceptable: pinchar --platform durante pull/run o en la programación de Kubernetes (si sabes lo que haces).
  3. Último recurso: ejecutar mediante emulación QEMU. Puede servir para desarrollo; raramente es una “solución” con neutralidad de rendimiento en producción.

Hechos e historia útiles para postmortems

  • Hecho 1: “Exec format error” es anterior a los contenedores; es un clásico error Unix/Linux devuelto cuando el kernel no puede cargar un formato binario.
  • Hecho 2: La historia multi-arch de Docker fue originalmente áspera; las “manifest lists” se convirtieron en el mecanismo mainstream para que una etiqueta refiera a múltiples arquitecturas.
  • Hecho 3: El movimiento de Apple a ARM (M1/M2/M3) incrementó drásticamente los incidentes de arquitectura equivocada porque los portátiles de desarrolladores dejaron de coincidir con muchos servidores de producción.
  • Hecho 4: Kubernetes no “arregla” el desajuste de arquitectura; programa pods en nodos, y los nodos ejecutan lo que se les da. El desajuste aparece como CrashLoopBackOff.
  • Hecho 5: La emulación user-mode de QEMU vía binfmt_misc es lo que hace “ejecutar imágenes ARM en x86” factible, pero sigue siendo emulación con sobrecarga real y casos límite.
  • Hecho 6: Alpine Linux usa musl libc; Debian/Ubuntu típicamente usan glibc. Enviar un binario ligado a glibc dentro de imágenes musl puede parecer “exec format error” o “no such file.”
  • Hecho 7: El encabezado ELF de un binario incluye la arquitectura. A menudo puedes diagnosticar el desajuste instantáneamente con file dentro de la imagen.
  • Hecho 8: “Funciona en mi máquina” obtuvo una nueva variante: “funciona en mi arquitectura.” La frase es más larga, pero la culpa es la misma.

Dónde se cuelan imágenes de arquitectura equivocada

Escenario A: compilar en portátiles ARM sin salida multi-arch

Un desarrollador construye una imagen en un portátil Apple Silicon. La imagen es linux/arm64. La empuja al registro bajo una etiqueta usada por CI.
En producción (x86_64), el binario del entrypoint es ARM. Boom: exec format error.

Escenario B: los runners de CI cambiaron de arquitectura en silencio

Migraron de runners self-hosted x86 a runners gestionados “más rápidos y baratos”. Sorpresa: la pool ahora incluye runners ARM.
Tu pipeline de build es determinista—solo que no de la manera que querías.

Escenario C: un Dockerfile multi-stage copió el binario equivocado

Los builds multi-stage son geniales. También son muy buenos para copiar el artefacto incorrecto eficientemente.
Si la etapa uno se ejecuta en una plataforma y la etapa dos en otra, puedes terminar con binarios desalineados embebidos en una base aparentemente correcta.

Escenario D: el script de entrypoint parece ejecutable pero no lo es

El entrypoint es un script de shell comiteado con finales de línea CRLF de Windows.
Linux intenta ejecutarlo, interpreta /bin/sh^M como intérprete, y obtienes una falla que se parece sospechosamente a problemas de arquitectura.

Escenario E: la etiqueta apunta a una imagen mono-arquitectura, no a una manifest list

Los equipos creen “publicamos multi-arch”. No lo hacen. Publican etiquetas separadas.
Entonces alguien usa la etiqueta “latest” en prod y obtiene la plataforma que el último build sobreescribió.

Broma #1: Los contenedores son como las máquinas de café de oficina: parecen estandarizados hasta que descubres que la mitad del edificio corre en pods incompatibles.

Tareas prácticas: comandos, salida esperada y decisiones

Esta sección es intencionalmente operativa. Cada tarea tiene tres partes: el comando, qué significa la salida, y qué decisión tomar a continuación.
Ejecuta esto desde un nodo donde ocurre la falla, o desde una estación con acceso a la imagen.

Task 1: Confirmar arquitectura del host (Linux)

cr0x@server:~$ uname -m
x86_64

Significado: La CPU del host es x86_64 (amd64). Si ves aarch64, eso es ARM64.

Decisión: Si el host es x86_64 y tu imagen es arm64, tienes un desajuste. Sigue para probarlo, luego arregla el proceso de build/publish.

Task 2: Confirmar arquitectura vía metadata del SO (más explícito)

cr0x@server:~$ dpkg --print-architecture
amd64

Significado: Nombre en familia Debian para la arquitectura. Esto ayuda cuando scripts o configs hablan en términos de distro.

Decisión: Mapear amd64x86_64, arm64aarch64. Si no coinciden con la plataforma de la imagen, ya sabes lo que viene.

Task 3: Inspeccionar la plataforma de la imagen que descargaste

cr0x@server:~$ docker image inspect --format '{{.Os}}/{{.Architecture}} {{.Id}}' myapp:prod
linux/arm64 sha256:5f9c8a0b6c0f...

Significado: La imagen local es linux/arm64. En un host amd64, no se ejecutará sin emulación.

Decisión: O bien tira la plataforma correcta explícitamente (corto plazo) o reconstruye/publica una imagen multi-arch (solución limpia).

Task 4: Inspeccionar la manifest list (a qué apunta realmente la etiqueta)

cr0x@server:~$ docker manifest inspect myorg/myapp:prod | sed -n '1,60p'
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
   "manifests": [
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1784,
         "digest": "sha256:9a1d...",
         "platform": {
            "architecture": "amd64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1784,
         "digest": "sha256:ab22...",
         "platform": {
            "architecture": "arm64",
            "os": "linux"
         }
      }
   ]
}

Significado: Esta etiqueta es una manifest list multi-arch. Eso es bueno. Si solo ves una manifest (no lista), es mono-arquitectura.

Decisión: Si la lista incluye la arquitectura de tu nodo, el registro está bien; tu pull podría estar forzado a otra plataforma o tu runtime hace algo raro.
Si no incluye tu arquitectura de nodo, debes publicar la que falta.

Task 5: Tirar la plataforma correcta explícitamente (movida de emergencia segura)

cr0x@server:~$ docker pull --platform=linux/amd64 myorg/myapp:prod
prod: Pulling from myorg/myapp
Digest: sha256:9a1d...
Status: Downloaded newer image for myorg/myapp:prod

Significado: Trajiste la variante amd64. El digest debe coincidir con el digest amd64 del manifest list.

Decisión: Si esto funciona, tu solución limpia sigue siendo hacer que la etiqueta resuelva correctamente sin pinchar manualmente la plataforma.

Task 6: Re-ejecutar el contenedor con plataforma explícita (diagnóstico)

cr0x@server:~$ docker run --rm --platform=linux/amd64 myorg/myapp:prod --version
myapp 2.8.1

Significado: La imagen corre cuando la plataforma es correcta. Esto implica fuertemente un desajuste de plataforma.

Decisión: Deja de depurar la aplicación en sí. Arregla la publicación de la imagen y la selección en despliegue.

Task 7: Encontrar el entrypoint y comando configurados

cr0x@server:~$ docker image inspect --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' myorg/myapp:prod
Entrypoint=["/usr/local/bin/entrypoint.sh"] Cmd=["myapp","serve"]

Significado: El ejecutable que falla probablemente sea /usr/local/bin/entrypoint.sh (o lo que aparezca aquí).

Decisión: Inspecciona ese archivo dentro de la imagen. No adivines.

Task 8: Inspeccionar el tipo de archivo del entrypoint dentro de la imagen

cr0x@server:~$ docker run --rm --entrypoint /bin/sh myorg/myapp:prod -lc 'ls -l /usr/local/bin/entrypoint.sh; file /usr/local/bin/entrypoint.sh'
-rwxr-xr-x 1 root root 812 Jan  2 10:11 /usr/local/bin/entrypoint.sh
/usr/local/bin/entrypoint.sh: POSIX shell script, ASCII text executable, with CRLF line terminators

Significado: Los terminadores de línea CRLF son una señal de alarma. El kernel puede atragantarse con la línea del intérprete.

Decisión: Convierte a LF en el repo o durante el build. Si es un binario, file te dirá la arquitectura.

Task 9: Inspeccionar la arquitectura ELF de un binario dentro de la imagen

cr0x@server:~$ docker run --rm --entrypoint /bin/sh myorg/myapp:prod -lc 'file /usr/local/bin/myapp; readelf -h /usr/local/bin/myapp | sed -n "1,25p"'
/usr/local/bin/myapp: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, not stripped
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Type:                              EXEC (Executable file)
  Machine:                           AArch64

Significado: Ese binario es ARM64. Si el host es amd64, este es tu indicio contundente.

Decisión: Reconstruye el binario para la plataforma correcta o publica builds multi-arch. No “arregles” esto cambiando el entrypoint.

Task 10: Detectar la trampa de “loader dinámico faltante”

cr0x@server:~$ docker run --rm --entrypoint /bin/sh myorg/myapp:prod -lc 'ls -l /lib64/ld-linux-x86-64.so.2 /lib/ld-musl-x86_64.so.1 2>/dev/null || true; ldd /usr/local/bin/myapp || true'
ldd: /usr/local/bin/myapp: No such file or directory

Significado: Que ldd reporte “No such file” para un archivo que claramente existe suele significar que falta la ruta del intérprete (loader dinámico) en el encabezado ELF.
Eso es un desajuste ABI/base-image, no un binario perdido.

Decisión: Asegura que tu imagen runtime coincida con las expectativas de libc (glibc vs musl) o entrega un binario estáticamente ligado cuando corresponda.

Task 11: Comprobar arquitectura y SO de nodos en Kubernetes

cr0x@server:~$ kubectl get nodes -o wide
NAME              STATUS   ROLES    AGE   VERSION   INTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
prod-node-a-01    Ready    worker   92d   v1.28.5   10.0.4.21     Ubuntu 22.04.3 LTS   5.15.0-91-generic  containerd://1.7.11

Significado: Esto no alcanza por sí mismo. Necesitas también la arquitectura.

Decisión: Consulta las etiquetas del nodo a continuación; Kubernetes codifica la arch como etiqueta.

Task 12: Confirmar etiqueta de arquitectura del nodo en Kubernetes

cr0x@server:~$ kubectl get node prod-node-a-01 -o jsonpath='{.metadata.labels.kubernetes\.io/arch}{"\n"}{.metadata.labels.kubernetes\.io/os}{"\n"}'
amd64
linux

Significado: El nodo es amd64. Si la imagen es solo arm64, los pods fallarán o nunca arrancarán.

Decisión: O programa hacia nodos coincidentes (nodeSelector/affinity) o publica la variante de imagen correcta. Prefiere publicar.

Task 13: Inspeccionar eventos del pod que falla en busca de pistas de exec/CrashLoop

cr0x@server:~$ kubectl describe pod myapp-7d6c7b9cf4-kkp2l | sed -n '1,120p'
Name:         myapp-7d6c7b9cf4-kkp2l
Namespace:    prod
Containers:
  myapp:
    Image:      myorg/myapp:prod
    State:      Waiting
      Reason:   CrashLoopBackOff
Events:
  Type     Reason     Age                    From               Message
  ----     ------     ----                   ----               -------
  Normal   Pulled     3m12s                  kubelet            Successfully pulled image "myorg/myapp:prod"
  Warning  BackOff    2m41s (x7 over 3m10s)  kubelet            Back-off restarting failed container

Significado: Kubelet extrajo la imagen con éxito; el contenedor muere tras intentar arrancar. Esto es compatible con exec format error.
Aún necesitas los logs del contenedor.

Decisión: Obtén los logs del contenedor (incluyendo el anterior) y revisa los mensajes del runtime.

Task 14: Recuperar los logs del contenedor previo (a menudo muestra el error)

cr0x@server:~$ kubectl logs myapp-7d6c7b9cf4-kkp2l -c myapp --previous
exec /usr/local/bin/myapp: exec format error

Significado: Esa es la negativa del kernel expuesta por el runtime.

Decisión: Confirma arch de imagen vs arch del nodo, luego procede a publicar una manifest list correcta.

Task 15: Para nodos con containerd, inspeccionar la plataforma de la imagen (si tienes acceso)

cr0x@server:~$ sudo crictl inspecti myorg/myapp:prod | sed -n '1,80p'
{
  "status": {
    "repoTags": [
      "myorg/myapp:prod"
    ],
    "repoDigests": [
      "myorg/myapp@sha256:9a1d..."
    ],
    "image": {
      "spec": {
        "annotations": {
          "org.opencontainers.image.ref.name": "myorg/myapp:prod"
        }
      }
    }
  }
}

Significado: La salida de crictl varía según runtime y configuración; no todos los setups exponen la plataforma directamente aquí.
El digest sigue siendo útil: puedes mapearlo a una entrada de manifiesto y ver qué plataforma se seleccionó.

Decisión: Si no puedes ver la plataforma aquí, confía en la inspección del manifiesto más las etiquetas de arquitectura del nodo.

Task 16: Verificar que BuildKit/buildx esté activo (lo quieres)

cr0x@server:~$ docker buildx version
github.com/docker/buildx v0.12.1 3b6e3c5

Significado: Buildx está instalado. Este es el camino moderno para builds multi-arch.

Decisión: Si falta buildx, instálalo/actívalo en CI. Deja de intentar parchear multi-arch con scripts caseros.

La solución limpia: construir y publicar imágenes multi-arch

La solución limpia es aburrida: compila para las plataformas en las que corres, publica una manifest list y deja que los clientes descarguen la variante correcta automáticamente.
Para eso sirven las etiquetas. Una etiqueta. Múltiples arquitecturas. Cero sorpresas.

Cómo se ve lo “limpio” en la práctica

  • Una etiqueta (p. ej., myorg/myapp:prod) apunta a una manifest list que incluye linux/amd64 y linux/arm64.
  • Cada imagen por plataforma se construye desde la misma revisión de código, con pasos reproducibles.
  • CI exige que la etiqueta publicada contenga realmente las plataformas requeridas.
  • El runtime no requiere overrides de --platform.

Construir y pushear multi-arch con buildx

En una máquina con Docker BuildKit y buildx, puedes construir y empujar en un solo paso:

cr0x@server:~$ docker buildx create --use --name multiarch
multiarch
cr0x@server:~$ docker buildx inspect --bootstrap | sed -n '1,120p'
Name:          multiarch
Driver:        docker-container
Nodes:
Name:      multiarch0
Endpoint:  unix:///var/run/docker.sock
Status:    running
Platforms: linux/amd64, linux/arm64, linux/arm/v7

Significado: Tu builder soporta múltiples plataformas. Si linux/arm64 no aparece, probablemente necesites configurar binfmt/QEMU.

Ahora construye y empuja:

cr0x@server:~$ docker buildx build --platform=linux/amd64,linux/arm64 -t myorg/myapp:prod --push .
[+] Building 128.4s (24/24) FINISHED
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 2.12kB
 => exporting manifest list myorg/myapp:prod
 => => pushing manifest for myorg/myapp:prod

Significado: La línea de log “exporting manifest list” es lo que quieres. Esa es la etiqueta multi-arch.

Decisión: Si esto produce solo una manifest, no construiste realmente multi-arch. Revisa las plataformas del builder y el entorno CI.

Cuándo necesitas binfmt/QEMU (y cuándo no)

Si tu máquina de build es amd64 y estás construyendo imágenes arm64 (o al revés), buildx puede usar emulación vía binfmt_misc.
Pero si puedes compilar de forma nativa en cada arquitectura (runners separados), eso suele ser más rápido y menos propenso a fallos.

cr0x@server:~$ docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
installing: arm64
installing: amd64

Significado: Esto registra handlers QEMU con el kernel para que binarios de arquitectura foránea puedan ejecutarse bajo emulación.

Decisión: Usa esto para habilitar builds multi-arch en un runner único. Para cargas en producción, no asumas que la emulación es aceptable solo porque arranca.

Dockerfiles multi-stage: mantener plataformas consistentes

La trampa más común es un build multi-stage donde la etapa builder y la etapa runtime no se alinean por plataforma, o copias un binario precompilado
descargado de internet que por defecto es amd64 mientras construyes arm64.

Haz explícita la plataforma y usa los build args que BuildKit provee:

cr0x@server:~$ cat Dockerfile
FROM --platform=$BUILDPLATFORM golang:1.22 AS build
ARG TARGETOS TARGETARCH
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/myapp ./cmd/myapp

FROM alpine:3.19
COPY --from=build /out/myapp /usr/local/bin/myapp
ENTRYPOINT ["/usr/local/bin/myapp"]

Significado: El builder corre en la plataforma de la máquina de build ($BUILDPLATFORM), pero el binario resultante se compila para la plataforma objetivo.

Decisión: Prefiere este patrón sobre “descargar un binario en el Dockerfile.” Si debes descargar, selecciona por TARGETARCH.

Endurecimiento: guardrails de CI que evitan reincidencias

Los incidentes causados por imágenes de arquitectura equivocada rara vez son “difíciles”. Son organizacionalmente fáciles de repetir.
Arreglar el build una vez no es lo mismo que prevenir el siguiente.

Guardrail 1: Afirmar que el manifiesto contiene las plataformas requeridas

Después de pushear, inspecciona el manifiesto y falla el pipeline si no es multi-arch (o si falta una plataforma requerida).

cr0x@server:~$ docker manifest inspect myorg/myapp:prod | grep -E '"architecture": "amd64"|"architecture": "arm64"'
            "architecture": "amd64",
            "architecture": "arm64",

Significado: Ambas plataformas aparecen. Si falta una, tu etiqueta está incompleta.

Decisión: Falla el build. No “avises y procedas.” Las advertencias son cómo se programan outages.

Guardrail 2: Registrar el digest de la imagen y desplegar por digest

Las etiquetas son punteros. Los punteros se mueven. Para producción, despliega digests inmutables cuando sea posible, especialmente para higiene de rollback.
Aún puedes publicar una etiqueta para humanos, pero deja que la automatización use digests.

cr0x@server:~$ docker buildx imagetools inspect myorg/myapp:prod | sed -n '1,60p'
Name:      myorg/myapp:prod
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest:    sha256:4e3f...
Manifests:
  Name:      myorg/myapp@sha256:9a1d...
  Platform:  linux/amd64
  Name:      myorg/myapp@sha256:ab22...
  Platform:  linux/arm64

Significado: Tienes un digest estable para la manifest list y digests por arquitectura debajo.

Decisión: Almacena el digest de la manifest list como el artefacto de despliegue. Es la unidad correcta para multi-arch.

Guardrail 3: Hacer de la plataforma un parámetro de primera clase en los builds

Si tu pipeline tiene variabilidad de arquitectura oculta, terminarás enviando los bits equivocados. Hazlo explícito.
Los jobs de build deben declarar qué plataformas se construyen y validan.

Guardrail 4: Dejar de permitir que portátiles de desarrolladores publiquen etiquetas de producción

Si un portátil puede publicar :prod, eventualmente tendrás un incidente de producción con el porcentaje de batería en la cadena causal.
Separa “push de dev” de “push de release.”

Guardrail 5: Validar el binario de entrypoint dentro de la imagen construída

Añade una comprobación de saneamiento post-build: ejecuta file sobre el binario principal por cada plataforma. Esto atrapa “copié el artefacto equivocado” incluso cuando el manifiesto parece correcto.

cr0x@server:~$ docker buildx build --platform=linux/amd64 -t myorg/myapp:test --load .
[+] Building 22.1s (18/18) FINISHED
 => => naming to docker.io/myorg/myapp:test
cr0x@server:~$ docker run --rm --entrypoint /bin/sh myorg/myapp:test -lc 'file /usr/local/bin/myapp'
/usr/local/bin/myapp: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped

Significado: El binario coincide con amd64. Repite para arm64 usando un runner nativo o una comprobación emulada si es aceptable.

Una cita que debería estar en toda pared de build: La esperanza no es una estrategia. — idea parafraseada común en círculos de operaciones.

Tres mini-historias corporativas (anonimizadas, dolorosamente familiares)

Mini-historia 1: El incidente causado por una suposición equivocada

Una compañía mediana corría la mayoría de sus workloads de producción en nodos Kubernetes amd64. Un pequeño pero creciente clúster de procesamiento por lotes usaba nodos arm64
porque eran más baratos para el perfil de rendimiento necesario. Ambos clústeres tiraban del mismo registro y usaban las mismas etiquetas de imagen.

Un equipo de servicio empujó una nueva release a última hora de la tarde. Construyeron en su propia flota de runners CI, que había sido amd64 por años.
Durante una iniciativa de ahorro, el grupo de plataforma CI añadió silenciosamente runners arm64 a la pool. El scheduler empezó a colocar builds en arm64 para algunos jobs.
Nadie lo documentó porque, desde su perspectiva, “no debería importar.”

La pipeline de Docker del equipo produjo una imagen mono-arquitectura. Cuando el job corrió en arm64, la etiqueta pusheada se volvió arm64-only.
El clúster de producción amd64 actualizó, tiró la etiqueta y empezó a crashear instantáneamente con exec format error.
Sonaron las alertas; el rollback no ayudó porque la etiqueta anterior ya había sido sobreescrita más temprano ese día.

El arreglo fue simple: reconstruir para amd64 y pushear de nuevo. La lección no fue simple: “La arquitectura de los runners CI es parte de tu cadena de suministro.”
Después del incidente introdujeron publicación multi-arch obligatoria y dejaron de permitir etiquetas mutables para despliegues de producción.
El cambio más valioso no fue técnico. Fue permiso: cualquier equipo podía bloquear una release si la manifest no era multi-arch.

Mini-historia 2: La optimización que salió mal

Un equipo de plataforma decidió acelerar builds cachéando artefactos compilados entre ejecuciones de pipeline. Idea razonable.
Introdujeron un bucket de cache compartido con llave basada en repo y branch, no en arquitectura. Aquí es donde la historia se vuelve cara.

Una semana después, un desarrollador en un portátil ARM ejecutó el build localmente, actualizando la cache como parte de una “mejora UX”.
CI tomó el artefacto cacheado, lo copió en la imagen final y publicó una imagen etiquetada como amd64 con un binario arm64 dentro.
La imagen base era amd64; el binario era arm64. Ese desajuste es un tipo especial de maldición porque la metadata miente mientras el kernel dice la verdad.

El incidente fue confuso. La inspección de imagen dijo linux/amd64. La arquitectura del nodo era amd64. Aun así el entrypoint fallaba.
Los ingenieros sospecharon capas corruptas, registros malos y “quizás Kubernetes está tirando lo equivocado.”
Finalmente alguien ejecutó file dentro del contenedor y obtuvo la verdad en una línea.

Mantuvieron el caching, pero arreglaron la llave: arquitectura y versión de toolchain pasaron a formar parte de la identidad de cache.
También añadieron una comprobación post-build que validaba la arquitectura del binario dentro de la imagen. El beneficio de rendimiento quedó; el coste sorpresa no.

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

Una empresa regulada tenía un hábito del que otras se burlaban: cada despliegue usaba digests, no etiquetas.
Los equipos se quejaban. Parecía incómodo en YAML. No era “moderno”. Era, sin embargo, extremadamente difícil de mutar por accidente.

Un equipo de aplicación envió una nueva versión construida en un workstation de desarrollador durante un hotfix urgente. Empujaron una etiqueta usada por staging.
La imagen era arm64. Staging era mixed-arch, y el rollout falló en la mitad de los nodos. Predecible.

Producción no se inmutó. Producción referenciaba un digest de manifest list producido por la pipeline oficial de release.
La etiqueta mutada nunca entró en la ruta de despliegue. El hotfix fue molesto, pero quedó contenido.
El equipo de plataforma no tuvo que “congelar etiquetas” ni jugar a golpear el registro. El proceso hizo su trabajo.

En la post-mortem, la empresa no se jactó. Simplemente apuntaron a la regla: “prod despliega solo desde artefactos firmados por digest de la pipeline.”
Nadie aplaudió. Nada se rompió. Ese es el punto.

Errores comunes: síntoma → causa raíz → arreglo

1) Pod CrashLoopBackOff con “exec format error” en logs

Síntoma: El contenedor arranca e inmediatamente sale; kubectl logs --previous muestra exec format error.

Causa raíz: La arquitectura de la imagen no coincide con la del nodo, o el binario principal dentro es de la arquitectura equivocada.

Arreglo: Publicar una imagen multi-arch (manifest list) y redeplegar; verificar etiquetas de arch del nodo y plataformas del manifiesto coincidentes.

2) Docker run falla localmente en Apple Silicon pero funciona en CI

Síntoma: Un desarrollador con M1/M2 ve exec format error al ejecutar una imagen construida/pullada en otro lugar.

Causa raíz: Imagen mono-arquitectura amd64 tirada en host arm64 sin emulación, o la etiqueta apunta solo a amd64.

Arreglo: Usar --platform=linux/arm64 temporalmente; a largo plazo publicar multi-arch. Si dependes de emulación, configúrala intencionalmente y mídela.

3) Image inspect dice amd64, aun así “exec format error”

Síntoma: docker image inspect muestra linux/amd64, pero el arranque falla con exec format error.

Causa raíz: Binario de arquitectura equivocada copiado dentro de la imagen (mix-up en multi-stage, cache, binario descargado).

Arreglo: Ejecuta file sobre el binario real del entrypoint dentro de la imagen; arregla el paso de build que inyecta el artefacto.

4) Script de entrypoint falla con exec format error, pero es “solo un script”

Síntoma: El entrypoint es un script de shell; el error aparece al inicio.

Causa raíz: Finales de línea CRLF o un shebang malo (ruta del intérprete inválida en la imagen).

Arreglo: Asegurar finales LF; asegurar que #!/bin/sh apunte a un intérprete existente; ejecutar file dentro de la imagen.

5) “No such file or directory” para un binario que existe

Síntoma: Logs muestran no such file or directory para el binario; ls muestra que el archivo existe.

Causa raíz: Intérprete ELF faltante (loader dinámico) o mismatch de libc (binario glibc en Alpine/musl).

Arreglo: Usar una imagen base compatible (basada en glibc) o compilar estático; validar ruta del intérprete vía readelf -l.

6) La etiqueta multi-arch existe, pero los nodos siguen tirando la variante equivocada

Síntoma: La manifest list incluye amd64 y arm64, pero un nodo jala la equivocada.

Causa raíz: Plataforma forzada vía --platform, configuración del runtime, o confusión por imagen/tag cacheada localmente.

Arreglo: Eliminar settings de plataforma forzada; tirar por digest; limpiar imágenes locales en el nodo si hace falta; verificar inspeccionando la plataforma de la imagen descargada.

Broma #2: “Exec format error” es la forma del kernel de decir, “Eso no es mi trabajo,” que también es mi forma favorita de declinar reuniones.

Listas de verificación / plan paso a paso

Paso a paso: respuesta a incidentes (15–30 minutos)

  1. Confirma la arquitectura de los nodos que fallan (uname -m o etiqueta del nodo en Kubernetes).
  2. Inspecciona la imagen que realmente tiró el nodo (docker image inspect o equivalente del runtime).
  3. Inspecciona la manifest del tag en el registro (docker manifest inspect).
  4. Determina si es un desajuste de plataforma o un desajuste interno de binario (file dentro de la imagen).
  5. Si hay desajuste: tira la plataforma correcta explícitamente como mitigación corta, o haz rollback a un digest conocido bueno.
  6. Comienza la solución limpia: reconstruir y pushear manifest list multi-arch.
  7. Añade una aserción en CI que verifique plataformas en el manifiesto y la arquitectura del binario del entrypoint.

Paso a paso: implementación de la solución limpia (mismo día)

  1. Habilitar BuildKit/buildx en CI y estandarizar su uso.
  2. Hacer Dockerfile multi-arch-safe: usar $BUILDPLATFORM, $TARGETARCH y compilar para el target.
  3. Construir y empujar: docker buildx build --platform=linux/amd64,linux/arm64 ... --push.
  4. Verificar que la manifest list contiene las plataformas requeridas.
  5. Smoke test por plataforma (runner nativo preferido; emulación aceptable para checks básicos).
  6. Desplegar usando el digest de la manifest list, no una etiqueta mutable.

Paso a paso: prevención (este sprint)

  1. Prohibir despliegues de producción desde etiquetas mutables; usar digests en entornos prod.
  2. Bloquear permisos del registro: solo CI puede pushear etiquetas de release.
  3. Hacer explícita la arquitectura de los runners en la planificación de CI; no permitir “pools mezcladas” sin builds multi-arch.
  4. Añadir un registro de procedencia de artefactos de build que incluya plataformas construidas y el digest de la manifest list.
  5. Enseñar al equipo: la arquitectura es parte de la interfaz, no un detalle de implementación.

Preguntas frecuentes

Q1: ¿Siempre es un problema de arquitectura de CPU “exec format error”?

No, pero es lo primero que revisar porque es común y rápido de probar. Los scripts con finales CRLF y shebangs malos también pueden dispararlo,
y los desajustes de intérprete ELF pueden parecer similares.

Q2: ¿Por qué Docker a veces “simplemente funciona” entre arquitecturas en mi portátil?

Porque podrías tener emulación QEMU registrada vía binfmt_misc, a menudo instalada por Docker Desktop o un paso previo de configuración.
Es conveniente. También puede ocultar problemas hasta que llegues a nodos de producción sin emulación.

Q3: ¿Cuál es la diferencia entre una imagen y una manifest list?

Una imagen única es el filesystem y la config de una sola plataforma. Una manifest list es un índice que apunta a múltiples imágenes específicas por plataforma bajo una etiqueta.
Los clientes seleccionan la imagen correcta según su plataforma (a menos que se fuerce lo contrario).

Q4: En Kubernetes, ¿puedo forzar la arquitectura correcta con node selectors?

Sí. Puedes usar kubernetes.io/arch en node selectors o reglas de affinity. Esto es útil cuando realmente ejecutas builds distintos por arch.
Pero no sustituye publicar imágenes multi-arch cuando la aplicación debe poder correr en cualquier entorno.

Q5: ¿Deberíamos usar --platform en despliegues de producción?

Solo como mitigación temporal o en situaciones estrictamente controladas. Se vuelve una política oculta que puede romper supuestos de scheduling y ocultar una mala publicación.
La solución a largo plazo son manifiestos correctos y builds correctos.

Q6: ¿Por qué ldd a veces dice “No such file” para un binario existente?

Porque el kernel no puede cargar el intérprete (loader dinámico) referenciado en los encabezados ELF. Esa ruta del loader no existe en la imagen,
a menudo por desajustes glibc/musl o paquetes de loader faltantes.

Q7: ¿Podemos publicar etiquetas separadas por arquitectura en lugar de manifests multi-arch?

Puedes, pero te arrepentirás a menos que tengas una disciplina estricta de nombrado, scheduling y despliegue. Las manifest multi-arch permiten que una sola etiqueta se comporte correctamente
entre entornos, lo que reduce el error humano—el recurso más abundante en la mayoría de organizaciones.

Q8: ¿Cuál es la prueba más rápida de que un binario está construido para la arquitectura equivocada?

Ejecuta file sobre él dentro del contenedor (o sobre el artefacto antes de empaquetarlo). Te dirá “x86-64” vs “ARM aarch64” de inmediato.
Sigue con readelf -h si necesitas más detalle.

Q9: Si construimos multi-arch, ¿tenemos que probar en ambas arquitecturas?

Sí. Al menos un smoke test. Los builds multi-arch pueden fallar de formas específicas por arquitectura: dependencias distintas, comportamiento de CGO o disponibilidad de librerías nativas.
“Se compiló” no es lo mismo que “se ejecuta.”

Conclusión: próximos pasos que perduran

Cuando veas el exec format error de Docker, trátalo como una alarma de incendio en producción: es ruidoso, es contundente y por lo general tiene razón.
No empieces reescribiendo entrypoints ni culpando a Kubernetes. Empieza confirmando la arquitectura en ambos lados y validando qué hay realmente dentro de la imagen.

Pasos prácticos siguientes:

  • Hoy: inspecciona la plataforma de la imagen que falla y el binario del entrypoint con docker image inspect y file.
  • Esta semana: publica una manifest list multi-arch usando buildx, y verifica eso en CI.
  • Este sprint: despliega por digest en producción y restringe quién puede pushear etiquetas de release.

La solución limpia no es ingeniosa. Es correcta. Y cuesta mucho menos que descubrir, otra vez, que tus CPUs tienen opiniones.

← Anterior
CSS moderno :has() en UI real: selector padre para formularios, tarjetas y filtros
Siguiente →
Eliminación de snapshots ZFS: por qué las instantáneas se niegan a desaparecer (y cómo solucionarlo)

Deja un comentario