m1 맥북을 사용하다보면, 맥북에서 빌드한 이미지가, 서로 다른 CPU 아키텍처로 인해 서버 에러에서 실행할 수 없는 경우가 발생합니다. 그래서, 이번 포스트에서는 다양한 플랫폼에서 지원되는 이미지를 빌드하는 방법에 대해 다루어 보았습니다.
Docker는 multi-architecture 빌드 등, 다양한 빌드 옵션을 지원하는 CLI 플러그인을 제공합니다. Buildx는 19.03 이후 버전부터 사용이 가능하다고 하니 버전 확인이 필요합니다. 공식 문서에 따르면, Docker Desktop을 사용하는 Windows나 MacOS 사용자 혹은 DEB, RPM 패키지로 도커를 설치한 사용자들은 자동으로 Buildx 플러그인이 포함되어 있습니다. docker buildx
명령어를 터미널에 입력했을 때, 다음과 같은 화면이 출력된다면 buildx를 사용할 수 있습니다.
$ docker buildx
Usage: docker buildx [OPTIONS] COMMAND
Extended build capabilities with BuildKit
Options:
--builder string Override the configured builder instance
Management Commands:
imagetools Commands to work on images in registry
Commands:
bake Build from a file
build Start a build
create Create a new builder instance
du Disk usage
inspect Inspect current builder instance
ls List builder instances
prune Remove build cache
rm Remove a builder instance
stop Stop builder instance
use Set the current builder instance
version Show buildx version information
Run 'docker buildx COMMAND --help' for more information on a command
일부 하위 버전에서는 buildx 플러그인을 사용하기 위해서는 ~/.docker/config.json
에 아래의 옵션을 추가해주어야 합니다. 아래 예제에서 사용할 docker manifest
와 같은 커맨드들을 사용하기 위해서도 아래의 옵션을 추가해야 합니다.
# ~/.docker/config.json
{
...
"experimental": "enabled"
}
Docker는 내부적으로 QEMU를 사용하여 빌드 환경을 에뮬레이트(emulate)
합니다. QEMU는 리눅스에서 사용하는 Hypervisor의 일종으로, 서로 다른 아키텍처 기반의 명령어를 번역(binary translation
)하여 커널에 전달하는 등, 가상의 하드웨어 환경이 존재하는 것처럼 꾸며내는 역할을 수행하는 에뮬레이터입니다.
QEMU는 내부적으로 binfmt_misc handler
라 불리는 리눅스 커널 기능을 사용합니다. 리눅스 커널은 현재 호스트 환경에서 해석할 수 없는 명령어(ex, 다른 아키텍처)를 만났을 때, 유저 스페이스에 해당 명령어를 처리할 수 있는 어플리케이션이 존재하는지 찾아보고, 만약 존재한다면 해당 어플리케이션이 해당 명령어를 처리하도록 합니다.
저는 m1 맥북에서, Docker Desktop을 사용하고 있는데, Docker Desktop에서는 이미 자주 사용되는 플랫폼의 환경설정이 되어 있습니다. 터미널에서 docker buildx ls
명령어를 실행하면, Docker Desktop에서 이미 제공하는 플랫폼들의 종류를 확인할 수 있습니다.
$ docker buildx ls
desktop-linux docker
desktop-linux desktop-linux running linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
default docker
default default running linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
리눅스 OS 사용자는 추가적으로 binfmt
을 사용하여 binfmt_misc handler
에 핸들러를 추가로 설치할 수도 있습니다.
$ docker run --privileged --rm tonistiigi/binfmt --install all
{
"supported": [
"linux/arm64",
"linux/amd64",
"linux/riscv64",
"linux/ppc64le",
"linux/s390x",
"linux/386",
"linux/mips64le",
"linux/mips64",
"linux/arm/v7",
"linux/arm/v6"
],
"emulators": [
"qemu-arm",
"qemu-i386",
"qemu-mips64",
"qemu-mips64el",
"qemu-ppc64le",
"qemu-riscv64",
"qemu-s390x",
"qemu-x86_64"
]
}
이제 Buildx를 사용하여 실제로 서로 다른 플랫폼을 위한 이미지를 빌드해보도록 하겠습니다.
먼저, 빌드된 이미지가 런타임 환경의 시스템 아키텍처를 출력하도록 다음과 같이 Dockerfile
을 작성합니다.
FROM debian:buster
CMD uname -m
베이스 이미지를 선택할 때에는 해당 이미지의 manifest를 확인하여 빌드하고 싶은 플랫폼을 지원하는지 여부를 확인해야 합니다. Docker는 특정 플랫폼의 이미지를 빌드할 때에, 자동으로 베이스 이미지에서 동일한 플랫폼으로 빌드된 이미지를 가져와서 사용하기 때문입니다. 예를 들어, x86 아키텍처를 지원하지 않는 베이스 이미지를 사용하여 동일한 아키텍처로 이미지를 빌드할 수 없습니다.
예제에서 사용한 debian:buster
이미지를 비롯하여 베이스 이미지로 널리 사용되는 이미지들은 대부분, 다양한 플랫폼을 지원합니다.
$ docker buildx imagetools inspect debian:buster
Name: docker.io/library/debian:buster
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest: sha256:fde7a280413ec0122bd3a14dc76ba152f89cae999f3b8efe8784100df3640763
Manifests:
Name: docker.io/library/debian:buster@sha256:5bb6e8c4f738d2da8662393543ddacdc8be9e421c46ce5316d2877c73c1fde16
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/amd64
Name: docker.io/library/debian:buster@sha256:59aca2b78d3884d0aed0aa32af61195dd0b96da1697fd4dfc64c5f504580a32c
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/arm/v5
Name: docker.io/library/debian:buster@sha256:705be950aa01d4efc68285cd6bdad9eb2ce1e64f67f95f6af3f5e0c12ebfc334
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/arm/v7
Name: docker.io/library/debian:buster@sha256:b9c2eac754faff5c52adbdadaef1ddef8c941c3e15060938e9d32db00472f82d
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/arm64/v8
Name: docker.io/library/debian:buster@sha256:77c49f5cbd69af85d855cae68e4975e6df56af852f766b3337a89cd5b0fc2654
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/386
Name: docker.io/library/debian:buster@sha256:97558357837f017e74d36a32e6977024ecf4e77a475ea487c8efa88a88928191
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/mips64le
Name: docker.io/library/debian:buster@sha256:e0737f57e6d1b26d1ff8a5a31dc08c453c2a918e029baab2be377459bc063446
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/ppc64le
Name: docker.io/library/debian:buster@sha256:0c4c4fd046c7eddcc3132f50dda341945d1e269a0556c0c8c03160e35aac04bb
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/s390x
다음으로, 별도의 설정없이 이미지를 빌드하고 컨테이너를 생성하였습니다. 저는 m1 맥북을 사용하고 있기 때문에 호스트 환경과 동일한 aarch64
가 출력되었습니다.
$ docker buildx build -t print-host-arch --load .
$ docker run --rm print-host-arch
aarch64
이번에는 platform=linux/amd64
옵션을 추가하여 이미지를 빌드한 후, 동일하게 실행하였습니다. 출력 결과를 살펴보면, 이전과 다르게 x86_64
(AMD64
의 실제 이름)를 출력하는 것을 확인할 수 있습니다.
$ docker buildx build --platform=linux/amd64 -t print-different-arch --load .
$ docker run print-different-arch
x86_64
마지막으로, platform=linux/amd64,linux/arm64
옵션을 추가하여 amd64
와 arm64
를 모두 지원하는 이미지를 빌드하였습니다. 이번에는 이미지를 빌드하고 푸시한 뒤 docker manifest inspect
명려어를 사용하여 이미지를 검사하였습니다. 출력 결과를 보면, 해당 이미지는 amd64
와 arm64
로 각각 빌드된 것을 확인할 수 있습니다.
$ docker buildx build --platform=linux/amd64,linux/arm64 -t <user-name>/print-arch --push .
$ docker manifest inspect --verbose <user-name>/print-host-arch
[
{
"Ref": "docker.io/<user-name>/print-arch:latest@sha256:4edf8cb577eb5ff3f2db876a947c6c50afdda11c33162441a3c4371721c0dbba",
"Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:4edf8cb577eb5ff3f2db876a947c6c50afdda11c33162441a3c4371721c0dbba",
"size": 528,
"platform": {
"architecture": "amd64", # !! amd64 !!
"os": "linux"
}
},
"SchemaV2Manifest": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:45ae9aad12b8a18933969e1fb418841ac14969f8635c3fa019bc19df5afcc398",
"size": 784
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:a024302f8a017855dd20a107ace079dd543c4bdfa8e7c11472771babbe298d2b",
"size": 50437057
}
]
}
},
{
"Ref": "docker.io/<user-name>/print-arch:latest@sha256:9cc943ec4351ce2e7a11e0ecbab4ec19ed45b7d67f9941228f361b4755688eb0",
"Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:9cc943ec4351ce2e7a11e0ecbab4ec19ed45b7d67f9941228f361b4755688eb0",
"size": 528,
"platform": {
"architecture": "arm64", # !! arm64 !!
"os": "linux"
}
},
"SchemaV2Manifest": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:68294e75b3f9ecbb98d4a7c50f8d59cfb13b0a4a09504afabbfd0e773a300a89",
"size": 780
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:ccd458f933f7966e412773ee1551aaf2433a5bf9adaae519e2ac7c9c3f8b5f89",
"size": 49223041
}
]
}
}
]
이번 포스팅에서는 Docker를 활용하여 다양한 플랫폼을 지원하는 이미지를 빌드하는 방법에 대해 다뤄보았습니다. 빌드된 이미지가 호스트 환경에 영향을 받는 것을 방지하기 위해, 이미지를 빌드하는 경우에는 --platform
옵션을 추가하여 사용하면 유용할 것 같습니다.