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.
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.
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.
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:
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.
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.
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%.