Thomas Jepp

Multi-platform builds with Gitlab CI (part 2)

Posted on

After using a M1 Mac Mini for a while, I upgraded my personal laptop to a M1 Max 14” MacBook Pro, and I’ve started using 64-bit ARM containers on AWS, Oracle Cloud, and Hetzner’s cloud service.

Previously, I used Docker Buildx and QEMU to build arm64 containers, but this is really slow - so I went looking at options to build natively.

GitLab CI provide an arm64 runner build, and Oracle Cloud have an “Always Free” Ampere option - with up to 24GB of RAM and up to 4 VCPUs.

This is how I set it up.

Preparing the x86_64 runner

  1. Install an OS on your runner. This is a VM on my own hardware, so I went with Debian.
  2. Install up to date Docker. Follow the official instructions.
  3. Install the Gitlab CI runner. Follow the official instructions.
  4. Register your runner. Follow the official instructions. Give it a unique tag - I used amd64.
  5. Add some default environment variables to /etc/gitlab-runner/config.toml:
    concurrent = 1
    check_interval = 0
    shutdown_timeout = 0
    
    [session_server]
      session_timeout = 1800
    
    [[runners]]
      name = "x86_64 runner"
      url = *snip*
      id = *snip*
      token = *snip*
      token_obtained_at = *snip*
      token_expires_at = *snip*
      executor = "docker"
      [runners.cache]
        MaxUploadedArchiveSize = 0
      [runners.docker]
        tls_verify = false
        image = "docker:stable"
        privileged = true
        disable_entrypoint_overwrite = false
        oom_kill_disable = false
        disable_cache = false
        volumes = ["/certs/client", "/cache"]
        shm_size = 0
    
  6. Restart the gitlab runner:
    systemctl restart gitlab-runner
    

Preparing the arm64 runner

  1. Install an OS on your runner. Oracle limit their free service to Oracle Linux or Ubuntu, so I went with Ubuntu.
  2. Install up to date Docker. Follow the official instructions.
  3. Install the Gitlab CI runner. Follow the official instructions.
  4. Register your runner. Follow the official instructions. Give it a unique tag - I used arm64.
  5. Add some default environment variables to /etc/gitlab-runner/config.toml:
    concurrent = 1
    check_interval = 0
    shutdown_timeout = 0
    
    [session_server]
      session_timeout = 1800
    
    [[runners]]
      name = "ARM64 OCI runner"
      url = *snip*
      id = *snip*
      token = *snip*
      token_obtained_at = *snip*
      token_expires_at = *snip*
      executor = "docker"
      [runners.cache]
        MaxUploadedArchiveSize = 0
      [runners.docker]
        tls_verify = false
        image = "docker:stable"
        privileged = true
        disable_entrypoint_overwrite = false
        oom_kill_disable = false
        disable_cache = false
        volumes = ["/certs/client", "/cache"]
        shm_size = 0
    
  6. Restart the gitlab runner:
    systemctl restart gitlab-runner
    

Making a multi-platform build

Use a template a bit like:

image: docker:latest

services:
   - docker:dind

before_script:
   - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

amd64-docker-build-latest:
   stage: build
   tags:
      - amd64
   script:
      - docker build -t "$CI_REGISTRY_IMAGE:latest-amd64" .
      - docker push "$CI_REGISTRY_IMAGE:latest-amd64"
   only:
      - master
      - main

arm64-docker-build-latest:
   stage: build
   tags:
      - arm64
   script:
      - docker build -t "$CI_REGISTRY_IMAGE:latest-arm64" .
      - docker push "$CI_REGISTRY_IMAGE:latest-arm64"
   only:
      - master
      - main

manifest-docker-build-latest:
   stage: deploy
   script:
      - docker manifest create "$CI_REGISTRY_IMAGE:latest" "$CI_REGISTRY_IMAGE:latest-amd64" "$CI_REGISTRY_IMAGE:latest-arm64"
      - docker manifest push "$CI_REGISTRY_IMAGE:latest"
   only:
      - master
      - main

amd64-docker-build:
   stage: build
   tags:
      - amd64
   script:
      - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-amd64" .
      - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-amd64"
   except:
      - master
      - main

arm64-docker-build:
   stage: build
   tags:
      - arm64
   script:
      - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm64" .
      - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm64"
   except:
      - master
      - main

manifest-docker-build:
   stage: deploy
   script:
      - docker manifest create "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-amd64" "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm64"
      - docker manifest push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
   except:
      - master
      - main

At this point, you should have a working multi-platform image!