Fixing “Exec Format Error” When Building ARM Docker Images on Intel Jenkins

Overview

If you are building Docker images for ARM architectures (like Apple Silicon or Raspberry Pi) on a standard Intel-based Jenkins server, you might have hit a wall that looks exactly like this:

> [3/6] RUN apt-get update && apt-get install -y espeak-ng:
0.188 exec /bin/sh: exec format error
Error building arm images on x86 cpu

I ran into this recently. My pipeline was working perfectly for weeks using docker buildx, and then suddenly—after adding a single RUN apt-get line—it exploded.

Here is why it happened and the simple “sidecar” fix for your Docker Compose setup.

  • The Host: An Intel (AMD64) server running Jenkins via Docker Compose.
  • The Target: An ARM64 Docker image (Python 3.12 on Debian Bookworm).
  • The Change: I modified the Dockerfile to install a system dependency (espeak-ng).

This was the most confusing part. My pipeline had been building this ARM image successfully for a while. Why did it fail now?

The answer lies in the difference between moving files and executing code.

  1. Before: My Dockerfile only used COPY instructions. Docker simply moves bytes from the host to the container image. The Intel CPU on the host handles this easily, regardless of the target architecture.
  2. After: I added RUN apt-get .... To run this, Docker has to spin up the container and execute the /bin/sh binary inside it.

Since the image is ARM64, the /bin/sh binary is written for an ARM processor. When my Intel server tried to run it, the CPU effectively said, “I don’t speak this language,” and threw the exec format error.

Here is my dockerfile:

# Start with an official Python base image.
# Using '-slim' provides a smaller image size.
FROM python:3.12.11-bookworm

# Set the working directory inside the container.
WORKDIR /app

RUN apt-get update && \
    apt-get install -y espeak-ng && \
    rm -rf /var/lib/apt/lists/*
# Copy the file that lists the dependencies.
# By copying this first, Docker can cache the installed packages layer
# and won't reinstall them unless requirements.txt changes.
COPY requirements.txt .

# Install build dependencies, then install python packages, then remove the build dependencies in a single layer.
# The 'fastuuid' package requires a Rust compiler (cargo) and a C linker (build-base) to build on ARM64.
# Using a virtual package (.build-deps) makes cleanup easy.
RUN pip install --no-cache-dir --upgrade pip -r requirements.txt

# Copy the rest of your application code from your local machine to the container.
COPY . .

# Expose the port the app runs on.
# FastAPI with Uvicorn defaults to 8000.
EXPOSE 8000

# Define the command to run your application.
# This tells uvicorn to run the 'app' instance from the 'main.py' file.
# --host 0.0.0.0 makes the app accessible from outside the container.
# --port 8000 matches the EXPOSE instruction.
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

The part

RUN apt-get update && \
    apt-get install -y espeak-ng && \
    rm -rf /var/lib/apt/lists/*

Was newly added, which caused the error.

The Solution: QEMU Emulation (binfmt)

To fix this, we need to teach the Intel Linux kernel how to understand ARM binaries. We do this using QEMU user-mode emulation and binfmt_misc.

We don’t need to change the Dockerfile. We just need to register the emulators on the Host machine.

Since my Jenkins runs via docker-compose.yml, the best way to handle this is to add a “sidecar” service that runs purely to register these emulators every time the stack starts.

Because Jenkins mounts the Docker socket (/var/run/docker.sock), it uses the Host’s kernel to run builds. Therefore, updating the Host’s kernel benefits Jenkins immediately.

Here is the updated docker-compose.yml:

services:
  # 1. The Fix: A service that registers QEMU emulators on the host
  qemu-register:
    image: tonistiigi/binfmt
    privileged: true
    command: --install all
    entrypoint: /usr/bin/binfmt

  # 2. Your existing Jenkins service
  jenkins:
    build: . 
    privileged: true
    user: root
    restart: always
    depends_on:
      - qemu-register # Ensure qemu runs first
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      # ... your other volumes

And now can build my image successfully again.

Build was successufl

Conclusion

In this post, I’ve shown you how to fix the issue of building arm images on x86 host.

Leave a Comment