Docker-образы стали стандартным инструментом для развертывания программного обеспечения. Docker предлагает принцип: "Build once, deploy anywhere." Звучит отлично, однако по умолчанию образы, которые мы создаем, работают на платформе linux/amd64 (x86-64)
. Это не учитывает пользователей других платформ, аудитория которых продолжает расти.
В индустрии происходят важные изменения: архитектура ARM набирает популярность. Все больше ноутбуков работают на ARM-процессорах, а устройства, такие как Raspberry Pi, становятся мощнее. Примером служит мой опыт использования Raspberry Pi 4 (8 ГБ), на котором мне удалось запустить TeamCity в Docker-контейнере
Также компания Apple с ее процессорами серии M также внесла существенный вклад в это изменение. Однако из пяти образов четыре мне пришлось пересобирать для работы на моем MacBook с процессором M1 Max.
Как же можно создавать образы, совместимые с различными архитектурами? Самый простой способ — это собрать образ на целевой платформе. Однако устройства вроде Raspberry Pi часто не обладают достаточной вычислительной мощностью для этого.
Для решения этой проблемы Docker внедрил концепцию мульти-архитектурных сборок. Мульти-архитектурный образ — это контейнер, который поддерживает несколько архитектур, и клиент Docker автоматически выбирает правильный вариант образа для вашей операционной системы и процессорной архитектуры.
Давайте рассмотрим, как можно создать такие образы двумя способами: с использованием Docker Manifest и docker buildx. Но сначала разберемся, что такое Docker Manifest.
Docker Manifest
Каждый Docker-образ сопровождается манифестом — это JSON-файл, который описывает все ключевые характеристики образа. Этот файл включает ссылки на слои образа, их размеры, контрольные суммы (хэши) и платформу, на которой образ должен запускаться.
Манифест помогает Docker правильно идентифицировать образ и позволяет ссылаться на него через тег, что делает поиск и использование образов более удобным.
Например, команда docker manifest inspect rustlang/rust:nightly-slim
покажет манифест образа Rust с тегом nightly-slim
:
$ docker manifest inspect --verbose rustlang/rust:nightly-slim
[
{
"Ref": "docker.io/rustlang/rust:nightly-slim@sha256:5acc1a41311e82d6ff877dac798f8283511d638883fa718a61dc396f4fd776b8",
"Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:5acc1a41311e82d6ff877dac798f8283511d638883fa718a61dc396f4fd776b8",
"size": 742,
"platform": {
"architecture": "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:38f09f84d5e6b1c55bbe525d63d5734892c01207f7d67f1282bdb7ac9b9920fb",
"size": 2492
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:6552179c3509e3c4314b4065e0d2790563d01cd474e2fdd58be4d46acd48af6a",
"size": 27153731
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:72f0c721929d948d14bcc5b4b383727a8242000ecb86f1c05b7facf0b735af9c",
"size": 262458494
}
]
}
},
{
"Ref": "docker.io/rustlang/rust:nightly-slim@sha256:ff4487a78b71c04f384c8232e489388e78d4bafc2b8a2ee11ff72471263a0724",
"Descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:ff4487a78b71c04f384c8232e489388e78d4bafc2b8a2ee11ff72471263a0724",
"size": 742,
"platform": {
"architecture": "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:a44e240b23915e620ff22072223d082d4d7c90cd608eb09301879ff21cc54604",
"size": 2491
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:4c7c9f6f1115fd4f35807a2f6c1375759365a991748aee0111873e55255f150b",
"size": 25923216
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:d05d169a780fb952804b80fa0472838866af0117b407b21fa3790eb4d291d77b",
"size": 306612829
}
]
}
}
]
Представьте, что файл манифеста мог бы содержать несколько манифестов, каждый из которых соответствует своей архитектуре. В таком случае Docker мог бы во время выполнения автоматически выбрать нужный манифест, соответствующий вашей системе.
Именно по такому принципу строится мульти-архитектурный образ. Такой манифест, который содержит список манифестов для разных архитектур, называется списком манифестов (manifest list).
Создаем Manifest руками
Начнем с более сложного подхода — создания Docker Manifest вручную. Сначала мы создадим образы для каждой архитектуры и отправим их в Docker Hub. Затем объединим эти образы в мульти-архитектурный манифест, который будет ссылаться на общий тег.
Для примера нам понадобится проект для сборки. Используем следующий Dockerfile
, который основан на Debian и добавляет в него утилиту curl
:
Обратите внимание на переменную ARCH
в Dockerfile
. При сборке мы будем указывать нужную базовую версию образа Debian для каждой архитектуры. Если бы мы оставили просто FROM debian:buster-slim
, Docker автоматически бы скачал образ Debian для архитектуры системы, на которой запущена сборка. Поэтому важно явно указывать архитектуру через аргумент ARCH
. Если для нужной архитектуры нет готового базового образа, его нужно будет создать вручную.
Теперь выполним команды для сборки и публикации образов для каждой архитектуры:
Сборка для AMD64:
$ docker build -t your-username/multiarch-example:manifest-amd64 --build-arg ARCH=amd64 .
$ docker push your-username/multiarch-example:manifest-amd64
Сборка для ARM32V7:
$ docker build -t your-username/multiarch-example:manifest-arm32v7 --build-arg ARCH=arm32v7 .
$ docker push your-username/multiarch-example:manifest-arm32v7
Сборка для ARM64V8:
$ docker build -t your-username/multiarch-example:manifest-arm64v8 --build-arg ARCH=arm64v8 .
$ docker push your-username/multiarch-example:manifest-arm64v8
Теперь, когда образы для разных архитектур собраны и опубликованы, мы можем объединить их в один мульти-архитектурный образ с помощью манифеста:
docker manifest create \
your-username/multiarch-example:manifest-latest \
--amend your-username/multiarch-example:manifest-amd64 \
--amend your-username/multiarch-example:manifest-arm32v7 \
--amend your-username/multiarch-example:manifest-arm64v8
После создания манифеста отправляем его в Docker Hub:
docker manifest push your-username/multiarch-example:manifest-latest
Теперь ваш мульти-архитектурный образ опубликован, и Docker автоматически выберет правильную архитектуру для конечной системы, на которой будет запущен образ.
Docker Buildix
Существует замечательный проект под названием QEMU, который способен эмулировать множество различных платформ. Благодаря недавним улучшениям в проекте Docker Buildx, использование QEMU с Docker стало как никогда простым.
Эмуляция QEMU в Docker основывается на функции ядра Linux под названием binfmt_misc. Эта функция позволяет Linux обрабатывать исполняемые файлы, предназначенные для других архитектур. Когда система сталкивается с форматом файла, который она не распознает (например, для другой архитектуры), она передает файл обработчику binfmt_misc
, который перенаправляет его в подходящее приложение, таким как эмулятор QEMU или виртуальная машина.
Чтобы это заработало, необходимо зарегистрировать интересующие нас платформы в системе. Если вы используете Docker Desktop, то платформы для большинства распространенных архитектур уже зарегистрированы. Однако, если вы работаете на Linux, регистрация осуществляется вручную. Для этого можно запустить образ docker/binfmt
с привилегиями:
$ docker run --privileged --rm docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
После этого может потребоваться перезапуск Docker. Если вам нужно зарегистрировать конкретные платформы или работать с экзотическими архитектурами, например, PowerPC, можно воспользоваться проектом qus.
Мы немного изменим наш предыдущий Dockerfile
, чтобы он стал более универсальным:
FROM debian:buster-slim
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl" ]
Чтобы использовать Docker Buildx для мульти-архитектурных сборок, необходимо сначала создать и активировать экземпляр сборщика:
docker buildx create --use
Теперь можно выполнить сборку образов для нескольких платформ с помощью одной команды:
$ docker buildx build \
--push \
--platform linux/arm/v7,linux/arm64/v8,linux/amd64 \ --tag your-username/multiarch-example:buildx-latest .
В команде сборки используется аргумент --platform
для указания архитектур, на которые мы нацеливаемся. Это позволяет Docker скачивать соответствующие образы для каждой платформы. Например, если в нашем Dockerfile
указан базовый образ FROM debian:buster
, Docker автоматически выберет образы Debian для всех указанных платформ (arm/v7, arm64/v8 и amd64), что упрощает процесс мульти-архитектурной сборки.
Этот метод эффективен, однако для более сложных сборок вы можете заметить, что его выполнение становится слишком медленным или возникают ошибки при работе с QEMU. В таких случаях целесообразно рассмотреть возможность использования кросс-компиляции образа, что может значительно ускорить процесс сборки и избежать проблем, связанных с эмуляцией.
Сборка в GitLab CI
Для завершения нашего обзора рассмотрим, как можно автоматизировать сборку контейнеров для различных архитектур с использованием GitLab CI. Пример покажет, как настроить процесс сборки и публикации образов в Docker Hub при пуше тега в формате v.*
.
Ниже приведен пример конфигурации .gitlab-ci.yml
для сборки мульти-архитектурного Docker-образа:
docker-build:
image: docker.struchkov.dev/docker-buildx:latest
stage: deploy
only:
- /^v.*$/
except:
- branches
services:
- docker:dind
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin
script:
- docker buildx create --use
- docker buildx build --push --platform linux/amd64,linux/arm64/v8 -t "$CI_REGISTRY_IMAGE:latest" -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" .
Не забудьте указать свои переменные GitLab CI.
CI_REGISTRY_USER
— имя пользователя для Docker Hub или другого регистра.CI_REGISTRY_PASSWORD
— пароль или токен доступа.CI_REGISTRY_IMAGE
— путь к образу в вашем реестре.
Заключение
Итак, мы выяснили, что не все пользователи Docker-образов работают на архитектуре amd64. Благодаря инструментам buildx и QEMU можно легко поддерживать различные архитектуры без значительных усилий. Эти технологии позволяют создавать мульти-архитектурные образы, которые автоматически подбираются под нужную платформу, что особенно важно для пользователей на ARM и других архитектурах.
Мы также рассмотрели, как автоматизировать этот процесс для репозиториев в GitLab CI, создавая мульти-архитектурные образы при пуше тегов. Однако аналогичный процесс можно реализовать и в других CI-системах, таких как Jenkins, CircleCI или GitHub Actions. Основные принципы остаются одинаковыми: использование buildx
для сборки и публикации образов и настройка аутентификации для Docker Registry.