I spent a couple of hours today working to build my Docker images for ARM and x64 machines in my hosted CI provider, CircleCI. Here is the solution I came up with. I hope this saves you some time if you have to do the same thing in the future.

This solution:

  • builds ARM and x64 images
  • runs on commits on the main branch of your repo
  • pushes your_app:latest and your_app:<1234567> image tags to Docker Hub (where <1234567> are the first 7 characters of the commit SHA)
  • if the commit is tagged, pushes your_app:<commit_tag> as well
version: 2.1

jobs:
  build:
    # We must use a machine image to get multi-arch support;
    # this doesn't work inside a Docker image
    resource_class: medium
    machine:
      image: ubuntu-2204:2022.07.1
    steps:
      - checkout
      - run:
          # Configure the "Docker Hub" for your organization, and set the DOCKER_LOGIN and DOCKER_PASSWORD env vars
          name: Sign in to Docker Hub
          command: docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD
      - run:
        # required for multi-arch builds
          name: Create Docker builder
          command: docker buildx create --use
      - run:
          name: Build and push image
          command: bin/build-and-push-image
          environment:
            IMAGE_NAME: your_username/your_image_name

workflows:
  build:
    jobs:
      - build:
          context:
            - Docker Hub
          filters:
            branches:
              only: main
Contents of .circleci/config.yml
#!/bin/bash

if [ -z "$CI" ]; then # protective magick
  echo "This script must be run inside a CI environment."
  exit 0
fi

export BUILD_CMD="docker buildx build --progress plain --platform linux/amd64,linux/arm64 --push"
BUILD_SHA="$(echo "$CIRCLE_SHA1" | cut -c1-7)"
export BUILD_SHA

set -euxo pipefail

$BUILD_CMD -t "$IMAGE_NAME:latest" .
$BUILD_CMD -t "$IMAGE_NAME:$BUILD_SHA" .

set +u
if [ -n "$CIRCLE_TAG" ]; then
  $BUILD_CMD -t "$IMAGE_NAME:$CIRCLE_TAG" .
fi
set -u
Contents of bin/build-and-push-image script