도커 고급 강좌 #2 멀티 아키텍처 이미지 — amd64와 arm64 한 묶음
Apple Silicon 머신이 일반화되고, AWS Graviton(ARM) 같은 ARM 운영 환경이 널리 쓰이면서 — 한 이미지가 두 아키텍처 모두에서 도는 일이 표준이 됐습니다. 이번 글은 그 흐름을 본격적으로 다룹니다.
도커 고급 강좌 시리즈에서 이번 글의 위치:
- #1 BuildKit과 buildx
- #2 멀티 아키텍처 이미지 — linux/amd64 + linux/arm64 ← 이번 글
- #3 이미지 보안 — non-root, distroless, scan(Trivy)
- #4 SBOM과 서명(cosign)
- #5 리소스 제한과 cgroups
- #6 프로덕션 운영 — restart 정책, healthcheck, graceful shutdown
가장 흔한 사고 — Apple Silicon ↔ amd64 #
이런 경험을 한 번쯤 합니다.
docker build -t myapp:1.0 .
docker push ghcr.io/me/myapp:1.0
# 운영 서버 (amd64)
docker pull ghcr.io/me/myapp:1.0
docker run myapp:1.0
# exec /myapp: exec format errorexec format error. 운영 서버가 받은 바이너리가 arm64 용 이라 amd64 CPU가 실행을 못 한 거입니다. Apple Silicon의 도커는 기본이 arm64 이미지를 만듭니다. 푸시할 땐 그걸 그대로 올렸고, 운영에서 받았을 땐 호환 가능한 변형이 없어서 arm64가 amd64 호스트에서 실행을 시도한 결과입니다.
해결은 두 갈래:
- 단일 amd64 이미지를 강제로 만든다 —
--platform linux/amd64 - 두 아키텍처를 모두 묶은 멀티 아키 이미지를 만든다 —
--platform linux/amd64,linux/arm64
운영 환경이 한 종류라면 1, 두 종류 이상이거나 미래에 어디로 갈지 모르면 2가 정석입니다. 요즘은 거의 항상 2.
Manifest list — 한 태그가 여러 이미지를 가리키는 구조 #
멀티 아키 이미지의 정체부터 봅시다. ghcr.io/me/myapp:1.0이라는 한 태그가, 사실은 여러 이미지를 묶은 manifest list (OCI 명세에선 image index) 일 수 있습니다.
ghcr.io/me/myapp:1.0 ← manifest list (image index)
│
├── linux/amd64 → image manifest @sha256:aaa...
├── linux/arm64 → image manifest @sha256:bbb...
└── linux/arm/v7 → image manifest @sha256:ccc...도커 클라이언트가 pull 할 때, 자기 호스트의 OS / 아키에 맞는 매니페스트를 자동으로 골라 다운받습니다. 사용자는 단일 태그 한 줄만 알면 됩니다.
docker buildx imagetools로 들여다보면 분명해집니다.
docker buildx imagetools inspect python:3.14
# Name: docker.io/library/python:3.14
# MediaType: application/vnd.oci.image.index.v1+json
# Digest: sha256:abc...
#
# Manifests:
# Name: docker.io/library/python:3.14@sha256:111...
# MediaType: application/vnd.oci.image.manifest.v1+json
# Platform: linux/amd64
#
# Name: docker.io/library/python:3.14@sha256:222...
# MediaType: application/vnd.oci.image.manifest.v1+json
# Platform: linux/arm64
#
# ... (linux/arm/v7, linux/386, linux/ppc64le, linux/s390x ...)공식 이미지들은 보통 6~8 개 아키텍처를 동시에 지원합니다.
빌드 — 두 아키 동시에 #
buildx + docker-container 드라이버 위에서 한 명령으로 만듭니다.
# 첫 번째: docker-container 드라이버 빌더 만들기 ([#1] 참고)
docker buildx create --name multi --driver docker-container --use --bootstrap
# 빌드 + 푸시
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/me/myapp:1.0 \
--push .여기서 짚어둘 두 사실:
--load는 단일 플랫폼만 가능. 멀티 플랫폼은--push또는--output type=oci,...로- 단일 호스트에서 두 아키를 만드는 건 한쪽이 emulation으로 돈다 — 그래서 느릴 수 있음
어떤 플랫폼이 가능한가 #
docker buildx inspect multi
# ...
# Platforms: linux/amd64, linux/arm64, linux/arm/v7,
# linux/arm/v6, linux/386, linux/ppc64le, linux/s390x기본 docker-container 드라이버는 BuildKit 컨테이너 안에 QEMU emulation이 같이 들어 있어 위 모든 플랫폼을 빌드할 수 있습니다. 호스트 CPU와 다른 아키는 emulation으로 도는 것입니다.
QEMU emulation — 가능한가, 빠른가 #
QEMU는 한 아키 위에서 다른 아키 코드를 돌리는 에뮬레이터입니다. 도커는 리눅스의 binfmt_misc와 QEMU를 묶어서, amd64 호스트에서 arm64 컨테이너를 돌릴 수 있게 해줍니다.
docker run --privileged --rm tonistiigi/binfmt --install allDocker Desktop은 이 등록을 자동으로 해 둡니다. 리눅스 CI 환경에선 명시적으로 해야 할 때가 있습니다.
비용 #
| 작업 | 같은 아키 | QEMU emulation |
|---|---|---|
apt-get install | 빠름 | 2~5배 느림 |
| 컴파일 (gcc, tsc) | 빠름 | 3~10배 느림 |
작은 스크립트 / COPY | 빠름 | 거의 동일 |
체감 차이는 빌드의 무게에 따라 큽니다. 가벼운 Python/Node 앱은 emulation만으로 충분하고, 무거운 컴파일이 들어가는 Rust/Go/C 빌드는 emulation이 매우 느립니다. 후자는 다음 절의 네이티브 빌더가 답입니다.
네이티브 멀티 아키 빌더 — --append
#
진지하게 멀티 아키 빌드를 굴린다면, 각 아키마다 네이티브 머신을 두는 게 가장 빠릅니다. buildx는 한 builder에 여러 노드(머신)를 등록하는 기능이 있습니다.
# 첫 노드 (amd64 머신에서)
docker buildx create --name native --node native-amd64 --platform linux/amd64
# 두 번째 노드 추가 (arm64 머신을 SSH로 등록)
docker buildx create --append \
--name native \
--node native-arm64 \
--platform linux/arm64 \
ssh://ubuntu@arm64-host
docker buildx use native
docker buildx inspect --bootstrap이 빌더로 --platform linux/amd64,linux/arm64 빌드를 돌리면, 각 플랫폼을 그 아키 머신이 네이티브로 빌드 합니다. 결과는 자동으로 manifest list로 묶입니다.
GitHub Actions에서는 ARM 러너가 무료로 제공되기 시작해서 (ubuntu-24.04-arm), 두 종류의 러너에서 빌드한 결과를 마지막에 합치는 패턴도 흔해졌습니다.
jobs:
build-amd64:
runs-on: ubuntu-24.04
# ... amd64 빌드, digest 출력
build-arm64:
runs-on: ubuntu-24.04-arm
# ... arm64 빌드, digest 출력
manifest:
needs: [build-amd64, build-arm64]
# docker buildx imagetools create로 두 digest를 한 manifest list로각 매트릭스 잡이 자기 아키만 네이티브로 빌드하고, 마지막에 묶기만 — 가장 빠른 멀티 아키 CI 패턴입니다.
docker buildx imagetools — 합치고 검증하는 도구
#
imagetools 명령은 이미지 / manifest list를 이미 빌드된 상태에서 다루는 도구입니다.
# 1) manifest list 검증 (위에서 본 명령)
docker buildx imagetools inspect ghcr.io/me/myapp:1.0
# 2) 두 이미지를 한 manifest list로 묶기
docker buildx imagetools create \
-t ghcr.io/me/myapp:1.0 \
ghcr.io/me/myapp:1.0-amd64 \
ghcr.io/me/myapp:1.0-arm64
# 3) 태그 옮기기 (가벼운 변경)
docker buildx imagetools create \
-t ghcr.io/me/myapp:latest \
ghcr.io/me/myapp:1.02 번이 위에서 본 GHA 패턴의 마지막 단계입니다. 각 잡이 별도 이미지로 푸시한 뒤, 마지막 잡이 imagetools create로 하나의 manifest list로 묶는 흐름을 정리합니다.
흔한 함정들 #
1. --platform 안 줘서 호스트 아키만 푸시됨
#
docker buildx build --push -t myapp . # 호스트 아키만 푸시됨docker-container 드라이버라도 --platform 안 주면 호스트 아키 한 개만 빌드/푸시합니다. 멀티 아키를 노린다면 항상 --platform을 명시해야 합니다.
2. 베이스 이미지가 한쪽만 지원함 #
FROM somebase:1.0somebase:1.0이 amd64만 있으면 arm64 빌드 시점에 manifest를 못 찾고 실패합니다. 빌드 전에 베이스의 플랫폼 지원을 확인:
docker buildx imagetools inspect somebase:1.0널리 쓰이는 공식 이미지들 (python, node, golang, nginx, postgres)은 보통 멀티 아키를 다 지원합니다.
3. 한쪽 아키에서만 통과하는 테스트 #
빌드 도중 RUN go test ... 같은 단계가 있다면, 두 아키에서 모두 도는지 확인해야 합니다. 어떤 라이브러리는 amd64-only 어셈블리를 쓰는 식의 경우가 있어, 한쪽에서만 깨질 수 있습니다.
4. 결과 검증 안 함 #
CI가 --push 후에 결과를 검증하지 않으면 버그를 못 잡습니다. 한 줄을 더하는 게 안전:
docker buildx imagetools inspect ghcr.io/me/myapp:${TAG} \
--format '{{range .Manifest.Manifests}}{{.Platform.OS}}/{{.Platform.Architecture}}{{"\n"}}{{end}}'
# linux/amd64
# linux/arm64기대한 플랫폼이 모두 들어 있는지 그냥 눈으로 확인하는 단계입니다.
한 컨테이너의 아키 강제하기 — --platform on run
#
런타임에 특정 아키를 강제로 받을 수도 있습니다.
docker run --platform linux/amd64 myappApple Silicon에서 amd64 이미지가 필요한 상황(예: amd64-only 인 어떤 데이터베이스 클라이언트)에 자주 씁니다. 이 경우 호스트가 emulation으로 돌리니 성능이 떨어집니다.
컴파일된 언어 — 정적 vs 동적 #
멀티 아키에서 한 번씩 만나는 지점입니다. 글리브-libc(glibc) 동적 링크 바이너리는 OS의 libc와 호환되어야 합니다. 정적 바이너리는 이 의존이 없어 한 번 빌드하면 어디서든 도는데, alpine의 musl libc와 glibc가 이런 경우에 충돌합니다.
+ Go (CGO_ENABLED=0) 정적 — glibc/musl 무관, 어디서든
+ Rust (musl target) 정적 — alpine 기반에서 자연스러움
+ Python wheel glibc/musl 별로 따로 만들어짐
+ Node native modules 같은 아키 + 같은 libc 여야
- glibc 컴파일된 C 확장 → musl(alpine)에서 안 돔멀티 아키 빌드 + slim 베이스(=glibc)가 일반 앱의 기본 조합입니다. alpine은 매력적이지만, 지금까지 본 요소들과 곱해지면 함정이 늘어납니다.
정리 #
이번 글에서 잡은 그림:
- 멀티 아키 이미지는 한 태그가 manifest list (OCI image index)로 여러 단일 매니페스트를 묶은 것
- **
docker-container드라이버 빌더 +--platform linux/amd64,linux/arm64**가 표준 패턴 - 멀티 플랫폼은
--load안 됨 —--push또는 OCI 출력 - QEMU emulation은 가능하지만 무거운 컴파일에서 느려짐. 진지하면 네이티브 멀티 아키 빌더(또는 GHA의 amd64+arm64 매트릭스 + imagetools 합치기)
- **
docker buildx imagetools**로 결과를 검증 / 합치기 / 태그 이동 - 베이스 이미지의 플랫폼 지원 / 한 아키에서만 도는 코드 / CI 검증 누락이 흔한 함정
다음 글(#3 이미지 보안 — non-root, distroless, scan(Trivy))에서는 이 빌드 인프라 위에 보안 감각을 얹습니다. 비특권 사용자, 읽기 전용 루트, capabilities drop, 그리고 Trivy 같은 도구로 이미지의 알려진 취약점을 스캔하는 방법입니다.