왜 멀티아키텍처가 문제가 되는가

M1/M2 맥(arm64)에서 docker build로 만든 이미지를 amd64 EC2에 배포하면 exec format error가 발생한다. 반대의 경우도 마찬가지다. 도커 이미지는 특정 CPU 아키텍처에 종속되기 때문이다. 과거에는 아키텍처별로 태그를 나눠(myapp:amd64, myapp:arm64) 관리했지만, 배포 시점에 태그를 골라야 하고 실수하기 쉽다. 이상적인 것은 하나의 태그로 여러 아키텍처를 담고, 런타임이 알아서 맞는 것을 내려받는 방식이다.

매니페스트 리스트라는 해법

OCI 이미지 규격에는 매니페스트 리스트(manifest list)가 있다. 하나의 태그가 여러 플랫폼별 이미지를 가리키는 인덱스 역할을 한다. 클라이언트가 docker pull myapp:latest를 하면 자신의 아키텍처에 맞는 이미지를 자동 선택한다. buildx는 이 매니페스트 리스트를 빌드와 동시에 만들어준다.

buildx 빌더 준비

기본 빌더는 멀티플랫폼을 지원하지 않으므로 docker-container 드라이버 기반 빌더를 새로 만든다. 크로스 빌드는 QEMU 에뮬레이션으로 처리하므로 binfmt 핸들러도 등록한다.

docker run --privileged --rm tonistiigi/binfmt --install all

docker buildx create --name multi --driver docker-container --use
docker buildx inspect --bootstrap

# 지원 플랫폼 확인
docker buildx inspect multi | grep Platforms

실제 빌드와 푸시

--platform에 대상 아키텍처를 나열하고, 결과를 레지스트리에 바로 올린다. 멀티플랫폼 결과는 로컬 도커 스토어에 담을 수 없어 --push 또는 --output이 필수다.

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag registry.example.com/myapp:1.4.0 \
  --tag registry.example.com/myapp:latest \
  --push \
  .

# 결과 매니페스트 확인
docker buildx imagetools inspect registry.example.com/myapp:1.4.0

빌드 방식 비교

방식속도정확도적합한 경우
QEMU 에뮬레이션느림(수 배)높음간단한 앱, 러너가 하나뿐일 때
네이티브 멀티노드빠름높음arm/amd 러너를 모두 갖춘 CI
크로스 컴파일빠름언어 의존Go/Rust 등 정적 빌드

QEMU는 설정이 가장 간단하지만 CPU 집약 빌드(예: 네이티브 확장 컴파일)에서 심하게 느려진다. Go처럼 크로스 컴파일이 쉬운 언어라면 TARGETPLATFORM을 받아 빌드 스테이지에서 GOOS/GOARCH를 지정하는 편이 훨씬 빠르다.

CI에서의 구성

GitHub Actions에서는 공식 액션으로 QEMU와 buildx를 세팅하고 캐시를 붙인다. 레이어 캐시가 없으면 매번 처음부터 빌드해 시간이 크게 늘어난다.

- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
  with:
    registry: registry.example.com
    username: ${{ secrets.REG_USER }}
    password: ${{ secrets.REG_PASS }}
- uses: docker/build-push-action@v6
  with:
    context: .
    platforms: linux/amd64,linux/arm64
    push: true
    tags: registry.example.com/myapp:${{ github.sha }}
    cache-from: type=registry,ref=registry.example.com/myapp:buildcache
    cache-to: type=registry,ref=registry.example.com/myapp:buildcache,mode=max

주의점

첫째, --load는 단일 플랫폼에서만 동작한다. 로컬 테스트가 필요하면 플랫폼을 하나만 지정해 로드하고, 최종 푸시는 별도로 한다. 둘째, 베이스 이미지가 대상 아키텍처를 지원하는지 확인해야 한다. 지원하지 않으면 해당 플랫폼 빌드가 실패한다. 셋째, Dockerfile에서 FROM --platform=$BUILDPLATFORM과 빌드 인자 TARGETPLATFORM을 활용하면 빌드는 네이티브로, 산출물만 대상 아키텍처로 만들 수 있어 에뮬레이션 비용을 줄인다. 넷째, docker manifest 명령은 실험적 기능이므로 buildx의 imagetools를 쓰는 편이 안정적이다. 마지막으로 캐시 키가 플랫폼별로 갈리므로, 캐시 히트율이 낮다면 mode=max와 레지스트리 캐시 조합을 점검한다.