Множественный RUN или одиночный RUN в Dockerfile, что лучше?

133

Dockerfile.1выполняет несколько RUN:

FROM busybox
RUN echo This is the A > a
RUN echo This is the B > b
RUN echo This is the C > c

Dockerfile.2 присоединяется к ним:

FROM busybox
RUN echo This is the A > a &&\
    echo This is the B > b &&\
    echo This is the C > c

Каждый RUNсоздает слой, поэтому я всегда предполагал, что чем меньше слоев, тем лучше и, следовательно Dockerfile.2, лучше.

Это, очевидно, верно, когда a RUNудаляет что-то добавленное предыдущим RUN(т.е. yum install nano && yum clean all), но в случаях, когда каждый RUNдобавляет что-то, есть несколько моментов, которые нам необходимо учитывать:

  1. Слои должны просто добавлять различие над предыдущим, поэтому, если более поздний слой не удаляет что-то добавленное в предыдущем, между обоими методами не должно быть большого преимущества в экономии места на диске ...

  2. Слои Dockerfile.1загружаются из Docker Hub параллельно, поэтому , хотя они, вероятно, немного больше, теоретически они будут загружаться быстрее.

  3. Если добавить 4-е предложение (т.е. echo This is the D > d) и локально перестроить, Dockerfile.1сборка будет быстрее благодаря кешу, но Dockerfile.2придется снова запустить все 4 команды.

Итак, вопрос: как лучше сделать Dockerfile?

Yajo
источник
1
В целом невозможно ответить, так как это зависит от ситуации и использования изображения (оптимизация по размеру, скорости загрузки или скорости сборки)
Генри

Ответы:

101

По возможности я всегда объединяю команды, создающие файлы, с командами, удаляющими те же файлы, в одну RUNстроку. Это связано с тем, что каждая RUNстрока добавляет слой к изображению, вывод - это буквально изменения файловой системы, которые вы можете просматривать docker diffво временном контейнере, который она создает. Если вы удаляете файл, который был создан на другом уровне, все, что делает файловая система union, - это регистрирует изменение файловой системы на новом уровне, файл все еще существует на предыдущем уровне, доставляется по сети и сохраняется на диске. Поэтому, если вы загружаете исходный код, извлекаете его, компилируете в двоичный файл, а затем удаляете tgz и исходные файлы в конце, вы действительно хотите, чтобы все это было сделано в одном слое, чтобы уменьшить размер изображения.

Затем я лично разделил слои на основе их потенциала для повторного использования в других изображениях и ожидаемого использования кеширования. Если у меня есть 4 образа, все с одним и тем же базовым образом (например, debian), я могу вытащить набор общих утилит для большинства этих образов в команду первого запуска, чтобы другие образы выиграли от кэширования.

Порядок в Dockerfile важен при рассмотрении повторного использования кэша изображений. Я смотрю на любые компоненты, которые будут обновляться очень редко, возможно, только когда обновится базовый образ и поместит их наверху в Dockerfile. Ближе к концу Dockerfile я включаю любые команды, которые будут выполняться быстро и могут часто меняться, например, добавление пользователя с определенным UID хоста или создание папок и изменение разрешений. Если контейнер включает интерпретируемый код (например, JavaScript), который активно разрабатывается, он добавляется как можно позже, так что при перестроении выполняется только это единственное изменение.

В каждой из этих групп изменений я максимально консолидирую их, чтобы минимизировать слои. Итак, если есть 4 разных папки с исходным кодом, они помещаются в одну папку, поэтому ее можно добавить с помощью одной команды. Любые пакеты, устанавливаемые из чего-то вроде apt-get, по возможности объединяются в один RUN, чтобы свести к минимуму накладные расходы менеджера пакетов (обновление и очистка).


Обновление для многоэтапных сборок:

Я гораздо меньше беспокоюсь об уменьшении размера изображения на незавершенных этапах многоступенчатой ​​сборки. Если эти этапы не помечены и не отправлены на другие узлы, вы можете максимизировать вероятность повторного использования кеша, разделив каждую команду на отдельную RUNстроку.

Однако это не идеальное решение для сжатия слоев, поскольку все, что вы копируете между этапами, - это файлы, а не остальные метаданные изображения, такие как настройки переменных среды, точки входа и команды. И когда вы устанавливаете пакеты в дистрибутиве Linux, библиотеки и другие зависимости могут быть разбросаны по файловой системе, что затрудняет копирование всех зависимостей.

Из-за этого я использую многоступенчатые сборки в качестве замены для сборки двоичных файлов на сервере CI / CD, так что моему серверу CI / CD требуется только инструмент для запуска docker build, а не jdk, nodejs, go и любые другие установленные инструменты компиляции.

BMitch
источник
30

Официальный ответ, указанный в их лучших практиках (официальные изображения ДОЛЖНЫ соответствовать этим)

Минимизируйте количество слоев

Вам необходимо найти баланс между удобочитаемостью (и, следовательно, долговременной ремонтопригодностью) Dockerfile и минимизацией количества используемых слоев. Будьте внимательны и осторожны с количеством используемых слоев.

Так как докер 1.10 COPY, ADDи RUNутверждение добавить новый слой к изображению. Будьте осторожны при использовании этих утверждений. Попробуйте объединить команды в один RUNоператор. Разделяйте его, только если это требуется для удобочитаемости.

Больше информации: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/minimize-the-number-of-layers

Обновление: несколько этапов в докере> 17.05

В многоэтапных сборках вы можете использовать несколько FROMоператоров в своем Dockerfile. Каждый FROMоператор является этапом и может иметь собственное базовое изображение. На последнем этапе вы используете минимальный базовый образ, например alpine, копируете артефакты сборки с предыдущих этапов и устанавливаете требования времени выполнения. Конечный результат этого этапа - ваш имидж. Итак, здесь вы беспокоитесь о слоях, как описано ранее.

Как обычно, в docker есть отличная документация по многоступенчатым сборкам. Вот небольшой отрывок:

При многоступенчатой ​​сборке вы используете несколько операторов FROM в своем Dockerfile. Каждая инструкция FROM может использовать другую базу, и каждая из них начинает новый этап сборки. Вы можете выборочно копировать артефакты с одного этапа на другой, оставляя на конечном изображении все, что вам не нужно.

Отличное сообщение в блоге об этом можно найти здесь: https://blog.alexellis.io/mutli-stage-docker-builds/

Чтобы ответить на ваши вопросы:

  1. Да, слои похожи на различия. Я не думаю, что добавляются слои, если абсолютно нулевых изменений. Проблема в том, что после того, как вы установили / загрузили что-то в слое №2, вы не можете удалить это в слое №3. Итак, как только что-то написано на слое, размер изображения больше не может быть уменьшен путем его удаления.

  2. Хотя слои можно тянуть параллельно, что потенциально ускоряет процесс, каждый слой, несомненно, увеличивает размер изображения, даже если они удаляют файлы.

  3. Да, кеширование полезно, если вы обновляете файл докера. Но работает в одном направлении. Если у вас есть 10 слоев, и вы меняете слой №6, вам все равно придется перестраивать все, начиная со слоя №6- №10. Так что не так часто это ускоряет процесс сборки, но гарантированно без надобности увеличивает размер вашего изображения.


Спасибо @Mohan за напоминание мне обновить этот ответ.

Мензо Вейменга
источник
1
Сейчас это устарело - см. Ответ ниже.
Mohan
1
@Mohan, спасибо за напоминание! Я обновил пост, чтобы помочь пользователям.
Menzo Wijmenga
19

Кажется, что ответы выше устарели. Примечание в документации:

До Docker 17.05 и даже больше, до Docker 1.10 было важно минимизировать количество слоев в вашем образе. Следующие улучшения смягчили эту потребность:

[...]

В Docker 17.05 и более поздних версиях добавлена ​​поддержка многоэтапных сборок, которые позволяют копировать только необходимые артефакты в окончательный образ. Это позволяет вам включать инструменты и отладочную информацию на промежуточных этапах сборки без увеличения размера окончательного образа.

https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#minimize-the-number-of-layers

и

Обратите внимание, что в этом примере также искусственно сжимаются две команды RUN вместе с помощью оператора Bash &&, чтобы избежать создания дополнительного слоя в изображении. Это неустойчиво и сложно поддерживать.

https://docs.docker.com/engine/userguide/eng-image/multistage-build/

Похоже, что лучшая практика изменилась на использование многоступенчатых сборок и сохранение Dockerfileчитабельности.

Mohan
источник
Хотя многоступенчатая сборка кажется хорошим вариантом для сохранения баланса, фактическое решение этого вопроса появится, когда docker image build --squashвариант выйдет за рамки экспериментального.
Yajo
2
@Yajo - Я скептически отношусь к отказу от squashэкспериментов. В нем много уловок, и он имел смысл только до многоэтапных сборок. В многоэтапных сборках вам нужно оптимизировать только заключительный этап, что очень просто.
Menzo Wijmenga
1
@Yajo Чтобы расширить это, только слои на последнем этапе имеют какое-либо значение для размера окончательного изображения. Так что, если вы поместите все свои куббины компоновщика на более ранние этапы, а на последнем этапе просто установите пакеты и скопируйте файлы с более ранних этапов, все будет работать прекрасно, и сквош не понадобится.
Mohan
3

Это зависит от того, что вы включаете в свои слои изображения.

Ключевой момент - разделить как можно больше слоев:

Плохой пример:

Dockerfile.1

RUN yum install big-package && yum install package1

Dockerfile.2

RUN yum install big-package && yum install package2

Хороший пример:

Dockerfile.1

RUN yum install big-package
RUN yum install package1

Dockerfile.2

RUN yum install big-package
RUN yum install package2

Другое предложение: удаление не так полезно, только если оно происходит на том же уровне, что и действие добавления / установки.

xdays
источник
Эти 2 действительно разделяют RUN yum install big-packageиз кеша?
Yajo 08
Да, они будут использовать один и тот же слой при условии, что они начнут с одной базы.
Ондра Жижка