Sep 25, 2024
 ]

Building Multi-Platform Docker Images for ARM64 in GitHub Actions

If you're building cross-platform Docker builds, it is important you use the right hardware to do this. Read more to ensure you have an optimal build environment for your Docker builds.
Aditya Jayaprakash
TL;DR
Use native ARM64 runners instead of slow QEMU emulation for faster builds and lower costs.
Get started!
Try us Free

At the heart of Docker's success are Docker images. A Docker image is a lightweight, standalone, executable package that includes everything needed to run a piece of software, including the code, runtime, libraries, environment variables, and config files. Images are built from Dockerfiles, which specify the steps needed to assemble the image.

Traditionally, most Docker images have been built for x86-64 architectures, which power most personal computers and servers. However, in recent years, Arm-based processors have seen a surge in popularity. Arm chips are known for their power efficiency, making them ideal for mobile devices and embedded systems. With the introduction of Arm-based server CPUs like AWS Graviton and the Apple M-series chip for Macs, Arm is now making inroads into personal computing and cloud infrastructure as well.

As a result, there's a growing need to build Docker images that can run on both x86-64 and Arm64 architectures. These multi-platform images allow developers to create containers that work seamlessly across a variety of hardware, from Intel-powered servers to Raspberry Pis and M-series Macs.

To build multi-platform images, you must use the docker buildx command with the --platform flag to specify the target architectures.

Building for x86-64

To build a Docker image for the x86-64 platform, you can use the following command:

docker buildx build --platform linux/amd64 -t myimage:amd64 .

This command uses the linux/amd64 platform flag to indicate that the image should be built for x86-64 systems.

Building for Arm64

Similarly, to build a Docker image for the Arm64 platform, you can use the linux/arm64 platform flag:

docker buildx build --platform linux/arm64 -t myimage:arm64 .

This will build an image compatible with Arm64 systems like the AWS Graviton or Apple M1.

The current problem: Using QEMU emulation

Since most folks run their CI on x86-64 machines, one common workaround for building Arm64 Docker images on x86-64 systems is to use QEMU (Quick EMUlator). QEMU is a generic open-source machine emulator and virtualizer that can emulate different CPU architectures, including Arm64.

You can use the official docker/setup-qemu-action to set up QEMU in your workflow for GitHub Actions. Here's an example:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      - name: Build and push
        uses: docker/build-push-action@v3
        with:
          platforms: linux/amd64,linux/arm64

While this approach does allow you to build Arm64 images on an x86-64 runner, it comes with significant drawbacks:

  1. Slow performance and increased build times: Emulating a different CPU architecture is inherently slower than running on native hardware. QEMU has to translate every Arm instruction into corresponding x86 instructions, which adds a lot of overhead. This can make the build process several times slower than native execution. Due to the slower performance, using QEMU emulation can drastically increase your overall build times, especially for larger or more complex Docker images. This can be frustrating for developers and can slow down your CI/CD pipeline.
  2. Higher costs: Longer build times also mean higher costs, as you consume more build minutes on the GitHub Actions runner. This can quickly add up if you're building large images or have a high volume of builds.
  3. Compatibility issues: While QEMU generally does a good job of emulating Arm64, compatibility issues or bugs that are difficult to diagnose and resolve can occasionally occur. Native execution eliminates these concerns.

Due to these limitations, using QEMU emulation for building Arm64 Docker images is, in our opinion, a last resort rather than an ideal solution. It's better suited for occasional or low-volume builds where the performance and cost penalties are less of a concern. For frequent or performance-critical builds, native Arm64 execution is strongly preferred.

A better way: Use Arm64 runners

The ideal solution for building Arm64 Docker images is to use a native Arm64 runner. This eliminates the performance overhead and compatibility issues associated with emulation and ensures that your builds run quickly and reliably.

Blacksmith provides managed Arm64 runners for GitHub Actions that make it easy to build Arm64 Docker images. Moreover, Blacksmith's runners are 50% cheaper than GitHub's runners, so you can also save on build costs.

We ran some benchmarks in our previous blog post and here are the results of with emulation vs running it on an Arm64 runner

Here's how you can use Blacksmith's Arm64 runners in your GitHub Actions workflow:

jobs:
  build:
    runs-on: blacksmith-2vcpu-ubuntu-2204-arm
    steps:
      - uses: actions/checkout@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      - name: Build and push
        uses: docker/build-push-action@v3
        with:
          platforms: linux/arm64

By specifying runs-on: blacksmith-2vcpu-ubuntu-2204-arm, you're directing GitHub Actions to use Blacksmith's 2vCPU Arm64 runner for this job. The subsequent steps set up Docker Buildx and build the Arm64 image just like before, but now it's running on native Arm64 hardware for better performance.

But what if you want to build images for both x86-64 and Arm64? You can use the include keyword in your workflow to define multiple jobs for each platform. Here's an example:


jobs:
  build:
    runs-on: ${{ matrix.runner }}
    strategy:
      matrix:
        platform: [amd64, arm64]
        include:
          - platform: amd64
            runner: blacksmith-2vcpu-ubuntu-2204
          - platform: arm64
            runner: blacksmith-2vcpu-ubuntu-2204-arm

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push Docker image
        run: |
          docker build --platform linux/${{ matrix.platform }} -t yourusername/yourimage:latest-${{ matrix.platform }} .
          docker push yourusername/yourimage:latest-${{ matrix.platform }}
          
   

The include keyword specifies the mapping between platform and runner, ensuring that AMD64 builds run on the blacksmith-2vcpu-ubuntu-2204 runner and ARM64 builds run on the blacksmith-2vcpu-ubuntu-2204-arm runner. Both jobs run natively on their respective platforms for the best possible performance.

Conclusion

Given the rise of Arm64 machines, Building Docker images for multiple architectures is necessary. However, using QEMU emulation on x86-64 runners to build Arm64 images introduces performance overhead and potential compatibility issues.

The most effective solution is to use native Arm64 machines to build Arm64 Docker images. Native execution on Arm64 hardware eliminates emulation overhead, ensuring optimal build speed and reliability. It also reduces the risk of encountering platform-specific bugs or inconsistencies.

Blacksmith's managed Arm64 runners for GitHub Actions make it simple to incorporate native Arm64 builds into your multi-platform Docker workflow while reducing your CI spending by 50%.

World globe

Start with 3,000 free minutes per month or book a live demo with our engineers