Multiplatform Dev Container Builds
May 15, 2026 ยท View on GitHub
Building dev containers to support multiple platforms (aka CPU architectures) is possible with the devcontainers/ci GitHub Action/Azure DevOps Task, but requires other actions/tasks to be run beforehand and has several caveats.
General Notes/Caveats
- Emulation-based multiplatform builds (using QEMU) will significantly increase build times over native, single architecture builds. For faster builds, consider using the native matrix strategy instead.
- If you are using runCmd, the command will only be run on the architecture of the system the build is running on. This means that, if you are using runCmd to test the image, there may be bugs on the alternate platforms that will not be caught by your test suite. Manual post-build testing is advised.
- GitHub Actions now offers hosted ARM runners (e.g.
ubuntu-24.04-arm). For Azure Pipelines, you will need a self-hosted ARM agent for native ARM builds.
GitHub Actions Example
name: 'build'
on:
pull_request:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout (GitHub)
uses: actions/checkout@v3
- name: Set up QEMU for multi-architecture builds
uses: docker/setup-qemu-action@v3
- name: Setup Docker buildx for multi-architecture builds
uses: docker/setup-buildx-action@v3
with:
use: true
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and release devcontainer Multi-Platform
uses: devcontainers/ci@v0.3
with:
imageName: ghcr.io/UserNameHere/ImageNameHere
platform: linux/amd64,linux/arm64
Azure DevOps Task Example
trigger:
- main
pool:
vmImage: ubuntu-latest
jobs:
- job: BuildContainerImage
displayName: Build Container Image
timeoutInMinutes: 0
steps:
- checkout: self
- task: Docker@2
displayName: Login to Container Registry
inputs:
command: login
containerRegistry: RegistryNameHere
- script: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
displayName: Set up QEMU
- script: docker buildx create --use
displayName: Set up docker buildx
- task: DevcontainersCi@0
inputs:
imageName: UserNameHere/ImageNameHere
platform: linux/amd64,linux/arm64
Native Multi-Platform Builds (Matrix Strategy)
Instead of using QEMU emulation on a single runner, you can use native runners in a matrix strategy. Each runner builds for its own architecture and pushes a platform-specific image. A separate merge action/task then combines the per-platform images into a single multi-arch manifest.
How it works
- Build jobs run in parallel on native runners. Each job sets
useNativeRunner: trueand a singleplatformvalue (e.g.,linux/amd64). The tag suffix is auto-derived from the platform (e.g.,linux/amd64becomeslinux-amd64). Build jobs must setpush: alwaysso that platform-specific images are pushed regardless of event filters (the merge job needs them in the registry). - Merge job runs after all build jobs complete. It uses a dedicated merge action (
devcontainers/ci/mergefor GitHub Actions,DevcontainersMergefor Azure DevOps) to combine the per-platform images into a multi-arch manifest. The platform-specific tags (e.g.,myimage:latest-linux-amd64) remain in the registry after the merge.
Benefits
- Faster builds -- no emulation overhead since each runner compiles natively.
- More reliable -- native compilation avoids QEMU compatibility issues.
- Flexible runners -- works with GitHub's hosted ARM runners (
ubuntu-24.04-arm) or self-hosted ARM agents.
GitHub Actions Example
In the workflow below, the build job is fanned out by the matrix into one run per entry: one runner builds and pushes linux/amd64, the other builds and pushes linux/arm64. Once both platform-specific images are in the registry, the manifest job merges them into one multi-arch tag.
jobs:
build:
strategy:
matrix:
include:
- runner: ubuntu-latest
platform: linux/amd64
- runner: ubuntu-24.04-arm
platform: linux/arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@v3
- uses: devcontainers/ci@v0.3
with:
imageName: ghcr.io/example/myimage
platform: ${{ matrix.platform }}
useNativeRunner: true
push: always
manifest:
needs: build
runs-on: ubuntu-latest
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@v3
- uses: devcontainers/ci/merge@v0.3
with:
imageName: ghcr.io/example/myimage
platforms: linux/amd64,linux/arm64
Note: The manifest job does not need
actions/checkoutsince no source code is accessed.
Azure DevOps Pipelines Example
stages:
- stage: Build
jobs:
- job: BuildAmd64
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
displayName: Login to Container Registry
inputs:
command: login
containerRegistry: RegistryNameHere
- script: docker buildx create --use
displayName: Set up docker buildx
- task: DevcontainersCi@0
inputs:
imageName: myregistry.azurecr.io/devcontainer
platform: linux/amd64
useNativeRunner: true
push: always
- job: BuildArm64
pool:
name: 'Self-Hosted-ARM64'
steps:
- task: Docker@2
displayName: Login to Container Registry
inputs:
command: login
containerRegistry: RegistryNameHere
- script: docker buildx create --use
displayName: Set up docker buildx
- task: DevcontainersCi@0
inputs:
imageName: myregistry.azurecr.io/devcontainer
platform: linux/arm64
useNativeRunner: true
push: always
- stage: Manifest
dependsOn: Build
jobs:
- job: MergeManifest
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
displayName: Login to Container Registry
inputs:
command: login
containerRegistry: RegistryNameHere
- script: docker buildx create --use
displayName: Set up docker buildx
- task: DevcontainersMerge@0
inputs:
imageName: myregistry.azurecr.io/devcontainer
platforms: linux/amd64,linux/arm64