Задавались ли вы когда-нибудь вопросом о том, почему размер Docker-контейнера, содержащего всего одно приложение, может находиться в районе 400 Гб? Или, может быть, вас беспокоили немаленькие размеры образа Docker, содержащего единственный бинарник размером в несколько десятков Мб?
Автор статьи, перевод которой мы сегодня публикуем, хочет разобрать основные факторы, влияющие на размеры контейнеров Docker. Он, кроме того, собирается поделиться рекомендациями по уменьшению размеров контейнеров.
Слои образов Docker
Образ контейнера Docker, в сущности, представляет собой набор файлов, наложенных друг на друга в несколько слоёв. Работающий контейнер собирается из этих файлов. Docker использует файловую систему UnionFS, в которой файлы группируются в виде слоёв. Слой может содержать один файл или несколько файлов, слои накладываются друг на друга. Во время выполнения контейнера содержимое слоёв объединяется, в результате конечный пользователь контейнера воспринимает материалы, «разложенные» по слоям, как единую файловую систему.
Упрощённое представление UnionFS
Получающаяся в итоге файловая система представляется конечному пользователю с помощью некоей реализации UnionFS (Docker поддерживает немало подобных реализаций через подключаемые драйверы хранилищ). Общий размер файлов, получаемых конечным пользователем, равен сумме размеров файлов, находящихся в слоях. Когда Docker создаёт на основе образа контейнер, он использует все слои образа, доступные только для чтения, добавляя поверх этих слоёв один тонкий слой, поддерживающий чтение и запись. Именно этот слой позволяет модифицировать файлы в работающем контейнере.
Выполняющийся контейнер содержит слой, поддерживающий чтение и запись, расположенный поверх слоёв, материалы которых доступны только для чтения
Что произойдёт, если в слое Layer 4
контейнера, схематично представленного выше, будет удалён некий файл? Хотя этот файл будет недоступен в файловой системе, которую видит пользователь, на самом деле, размер этого файла всё ещё будет одной из составляющих размера контейнера, так как этот файл останется в одном из слоёв, предназначенных только для чтения.
Довольно просто начать сборку образа с маленького исполняемого файла приложения и дойти до образа очень большого размера. Ниже мы рассмотрим различные методы, позволяющие делать контейнеры настолько маленькими, насколько это возможно.
Обращайте внимание на путь к папке, на основе материалов которой собирают образы
Как чаще всего собирают образы Docker? Видимо — вот так:
docker build .
Точка в этой команде сообщает Docker о том, что мы считаем текущую рабочую директорию корнем файловой системы, использующейся в процессе сборки образа.
Для того чтобы лучше понять то, что происходит после выполнения вышеописанной команды, стоит помнить о том, что сборка образа Docker — это клиент-серверный процесс. Интерфейс командной строки Docker (клиент), которому мы даём команду docker build
, использует движок Docker (сервер) для сборки образа контейнера. Для ограничения доступа к базовой файловой системе клиента системе сборки образа нужно знать о том, где находится корень виртуальной файловой системы. Именно там инструкции из файла Dockerfile
осуществляют поиск файловых ресурсов, которые могут, в итоге, попасть в собираемый образ.
Представим себе место, в котором обычно размещают файл Dockerfile
. Вероятно, это — корневая директория проекта? Если в корне проекта находится Dockerfile
, который используется командой docker build
для сборки образа, то окажется, что в образ могут попасть все файлы проекта. Это способно привести к тому, что в контекст сборки образа могут попасть тысячи ненужных файлов размером в многие мегабайты. Если легкомысленно использовать в Dockerfile
команды ADD
и COPY
, то все файлы проекта вполне могут стать частью готового образа. Чаще всего тем, кто собирает образы, это не нужно, так как в итоговый образ обычно должны входить лишь некоторые, избранные файлы.
Всегда обращайте внимание на то, чтобы команда docker build
получала бы правильный путь, и на то, чтобы в Dockerfile
не было бы команд, добавляющих в образ ненужные файлы. Если по каким-либо причинам корень проекта необходимо сделать контекстом сборки, можно выборочно включать в него файлы и исключать их из него, пользуясь .dockerignore
.
Оптимизируйте слои образа
Максимальное количество слоёв, которое может иметь образ, равняется 127 (при учёте поддержки такого количества слоёв используемым драйвером хранилища данных). Это ограничение, в случае крайней необходимости, может быть ослаблено, но при таком подходе сужается спектр систем, на которых можно собирать подобные образы. Речь идёт о том, что движок Docker должен выполняться на системе, ядро которой модифицировано соответствующим образом.
Как уже было сказано в предыдущем разделе, из-за того, что при сборке образов используется UnionFS, файлы, попадающие в некий слой, остаются там даже в том случае, если они были удалены из вышележащих слоёв. Разберём это, прибегнув к экспериментальному файлу Dockerfile:
FROM alpine
RUN wget http://xcal1.vodafone.co.uk/10MB.zip -P /tmp
RUN rm /tmp/10MB.zip
Соберём образ:
Сборка экспериментального образа, в котором имеется нерационально используемое пространство
Исследуем образ с помощью dive:
Показатель эффективности образа составляет 34%
Показатель эффективности образа в 34% указывает на то, что немалый объём пространства образа используется нерационально. Это ведёт к увеличению времени загрузки образа, к ненужным тратам сетевых ресурсов, к замедлению времени запуска контейнера.
Как избавиться от этой проблемы? Рассмотрим несколько вариантов.
▍Слияние результатов работы команд
Доводилось ли вам когда-нибудь видеть файлы Dockerfile
, содержащие очень длинные директивы RUN
, в которых множество команд оболочки объединены с помощью &&
? Это и есть слияние результатов работы команд.
Пользуясь этим методом, мы создаём, на основе результатов работы единственной длинной команды, лишь один слой. Так как в образе не будет слоёв, содержащих файлы, удалённые в следующих слоях, итоговый образ не будет включать в себя такие «файлы-призраки». Рассмотрим это на примере, приведя вышеприведённый Dockerfile
к такому состоянию:
FROM alpine
RUN wget http://xcal1.vodafone.co.uk/10MB.zip -P /tmp && rm /tmp/10MB.zip
После этого проанализируем образ:
Слияние команд позволило создать образ, оптимизированный на 100%
Применение этой методики оптимизации размеров образов на практике заключается в том, что после завершения работы над файлом Dockerfile
его нужно проанализировать и выяснить, можно ли воспользоваться слиянием команд для уменьшения объёма нерационально используемого пространства.
▍Применение опции --squash
В тех случаях, когда пользуются чужими файлами Dockerfile
, которые не хочется или невозможно изменить, альтернативой слиянию команд может стать сборка образа с использованием опции --squash
.
Современные версии Docker (начиная с 1.13) позволяют сводить все слои в один слой, избавляясь тем самым от «призрачных ресурсов». При этом можно пользоваться исходным неизменённым Dockerfile
, содержащим множество отдельных команд. Но собирать образ нужно с использованием опции --squash
:
docker build --squash .
Образ, получающийся в итоге, тоже оказывается оптимизированным на 100%:
Использование опции --squash при сборке позволило создать образ, оптимизированный на 100%
Тут можно обратить внимание на одну интересную деталь. А именно, в Dockerfile
был создан слой для добавления файла и ещё один слой для удаления этого файла. Опция --squash
достаточно интеллектуальна для того, чтобы понять, что при таком раскладе вообще не нужно создавать дополнительных слоёв (в итоговом образе имеется только слой 9ccd9…
из используемого нами базового образа). В общем, за это мы можем поставить --squash
дополнительный плюс. Правда, используя --squash
, нужно учитывать, что это может помешать воспользоваться кэшированными слоями.
В итоге рекомендуется учитывать то, что работая с чужими Dockerfile
, которые вам не хотелось бы менять, вы можете минимизировать объём нерационально используемого пространства образов, собирая образы с применением опции --squash
. Для анализа готового образа можно воспользоваться инструментом dive.
Удаляйте кэши и временные файлы
При контейнеризации приложений часто складывается такая ситуация, когда вместе с ними нужно поместить в образ дополнительные инструменты, библиотеки, утилиты. Это делается с помощью пакетных менеджеров вроде apk
, yum
, apt
.
Пакетные менеджеры стремятся к тому, чтобы сберечь время пользователя и не нагружать лишний раз его сетевое подключение при установке пакетов. Поэтому они кэшируют загружаемые данные. Для того чтобы размер итогового образа Docker был бы как можно меньше, нам не нужно хранить в этом образе кэши менеджеров пакетов. В конце концов, если нам когда-нибудь понадобится другой образ, его всегда можно пересобрать с использованием обновлённого Dockerfile
.
Для того чтобы удалить кэши, создаваемые тремя вышеупомянутыми популярными пакетными менеджерами, в конец агрегированной команды (то есть такой, выполнение которой приводит к созданию одного слоя) можно добавить следующее:
APK: ... && rm -rf /etc/apk/cache
YUM: ... && rm -rf /var/cache/yum
APT: ... && rm -rf /var/cache/apt
В результате рекомендуется перед завершением работы над Dockerfile
внести в него команды, убирающие кэши пакетных менеджеров, используемых при сборке образа. Это же относится и к любым временным файлам, которые не влияют на правильность работы контейнера.
Тщательно выбирайте базовый образ
Каждый Dockerfile
начинается с директивы FROM
. Именно тут мы задаём базовый образ, на основе которого будет создан наш образ.
Вот что говорит об этом документация Docker: «Инструкция FROM
инициализирует новый этап сборки и устанавливает базовый образ для идущих далее инструкций. В результате правильно составленный Dockerfile
должен начинаться с инструкции FROM
. Образом может быть любой работоспособный образ. Легче всего приступить к сборке собственного образа, взяв за его основу образ из общедоступного репозитория».
Очевидно, существует масса базовых образов, каждый из которых отличается собственными особенностями и возможностями. Правильный подбор базового образа, содержащего именно то, что нужно приложению, не больше и не меньше, оказывает огромнейшее воздействие на размер итогового образа.
Как и можно ожидать, размеры популярных базовых образов чрезвычайно сильно варьируются:
Размеры популярных базовых образов Docker
Так, контейнеризация приложения с использованием базового образа Ubuntu 19.10 приведёт к тому, что к размеру образа, помимо размера приложения, будет добавлено ещё 73 Мб. Если собрать такой же образ на основе образа Alpine 3.10.3, то мы получим «добавку» лишь в размере 6 Мб. Так как Docker кэширует слои образов, сетевые ресурсы тратятся на загрузку того или иного образа лишь тогда, когда в первый раз запускают контейнер с соответствующим образом (проще говоря — при первой загрузке образа). Но размер самого образа меньше от этого не становится.
Тут вы можете прийти к следующему (совершенно логичному) выводу: «Значит — всегда буду использовать Alpine!». Но, к сожалению, в мире разработки программного обеспечения не всё так однозначно.
Может быть, разработчики Alpine Linux обнаружили какой-то секретный ингредиент, который всё ещё не могут найти представители Ubuntu или Debian? Нет. Дело в том, что для того, чтобы создать образ Docker, размер которого на порядок меньше размера образа той же Debian, разработчикам Alpine пришлось принять некоторые решения относительно того, что нужно включить в образ, а что — не нужно. Прежде чем называть Alpine базовым образом, который вы будете использовать всегда, вам стоит поинтересоваться тем, имеется ли в нём всё то, что вам нужно. Кроме того, даже хотя в Alpine есть менеджер пакетов, может оказаться так, что конкретный пакет, который используется в вашем рабочем окружении, основанном, например, на Ubuntu, в Alpine недоступен. Или — не пакет, а нужная версия пакета. Это — те компромиссы, о которых стоит знать перед выбором и испытанием базового образа, наилучшим образом подходящего для вашего проекта.
И наконец, если вам и правда нужен один из самых крупных базовых образов, вы можете воспользоваться инструментом для минимизации размеров образа. Например — бесплатным опенсорсным средством DockerSlim. Это позволит уменьшить размер готового образа.
В итоге можно сказать, что использование тщательно подобранного базового образа чрезвычайно важно в деле создания собственных компактных образов. Оцените нужды вашего проекта и подберите образ, который содержит то, что вам необходимо, и при этом имеет приемлемые для вас размеры.
Рассмотрите возможность создания образа, в котором нет базового образа
Если ваше приложение может выполняться без некоего дополнительного окружения, предоставляемого базовым образом, вы можете решить не использовать базовый образ. Конечно, так как инструкция FROM
обязательно должна присутствовать в Dockerfile
, без неё обойтись не получится. Она должна, кроме того, указывать на какой-то образ. Какой же образ использовать в такой ситуации?
Здесь вам может пригодиться образ Scratch. Из его описания можно узнать о том, что он специально сделан пустым и рассчитан на построение образов, если «говорить» языком Dockerfile
, «FROM scratch
», то есть — «с нуля». Этот образ особенно полезен при создании базовых образов (таких, как образы debian и busybox) или предельно минималистичных образов (тех, которые содержат единственный бинарный файл и то, что требуется для его работы, скажем, это нечто вроде hello-world). Использование этого образа в качестве основы образа, описываемого Dockerfile
, аналогично применению в некоей программе «пустой операции». Применение образа scratch
не приведёт к созданию в готовом образе дополнительного слоя.
В результате, если ваше приложение представляет собой самодостаточные исполняемые файлы, которые могут работать сами по себе, выбор базового образа scratch
позволит вам до предела минимизировать размер контейнера.
Используйте многоэтапные сборки
Многоэтапные сборки были в центре внимания после выхода Docker 17.05. Это была возможность, которую ждали уже давно. Она позволяет сборщикам образов отказаться от собственных скриптов для сборки образов и реализовать всё что нужно, используя хорошо известный формат Dockerfile
.
В общих чертах многоэтапную сборку можно представить себе в виде объединения нескольких Dockerfile
, или в виде Dockerfile
, в котором имеется несколько инструкций FROM
.
До появления многоэтапных сборок, если нужно было создать сборку своего проекта и распространить её в контейнере с использованием Dockerfile
, то, вероятно, потребовалось бы выполнять процесс сборки, который привёл бы к появлению контейнера, наподобие того, который показан ниже:
Сборка и распространение приложение без использования технологии многоэтапной сборки
Хотя, с технической точки зрения, тут всё сделано правильно, итоговый образ и получающийся в результате контейнер наполнены слоями, созданными в процессе подготовки материалов проекта. А эти слои не нужны для формирования среды выполнения проекта.
Многоэтапные сборки позволяют отделить фазы создания и подготовки материалов проектов от окружения, в котором выполняется код проектов.
Многоэтапная сборка, отделение процесса создания и подготовки материалов проекта от среды выполнения
При этом для описания полного процесса сборки проекта достаточно единственного Dockerfile
. Но теперь можно копировать материала из одного этапа в другой и избавляться от ненужных данных.
Многоэтапные сборки позволяют создавать кросс-платформенные сборки, которыми можно пользоваться многократно без применения собственных сборочных скриптов, написанных под конкретную операционную систему. Итоговый размер образа может быть сведён к минимуму благодаря возможности избирательного включения в него материалов, сгенерированных на предыдущих этапах процесса сборки образа.
Итоги
Создание образов контейнеров Docker — это процесс, с которым часто приходится сталкиваться современным программистам. Существует множество ресурсов, посвящённых созданию файлов Dockerfile
, в интернете можно найти много примеров таких файлов. Но, чем бы вы ни пользовались, создавая собственные Dockerfile
всегда стоит учитывать то, каким будет размер итоговых образов.
Здесь мы рассмотрели несколько методик минимизации размера образов Docker. Внимательное отношение к содержимому Dockerfile
, включение в него только того, что действительно нужно, выбор подходящего базового образа, использование технологии многоэтапной сборки — всё это способно помочь серьёзно сократить размер создаваемых вами образов Docker.
P.S. Мы запустили маркетплейс на сайте RUVDS. В маркетплейсе образ Docker устанавливается в один клик, вы можете проверить как работают контейнеры на виртуальном сервере, даем 3 дня бесплатного теста для всех новых клиентов.
Уважаемые читатели! Как вы оптимизируете размеры ваших образов Docker?
Автор: ru_vds