Эта публикация - текстовый вариант и сценарий для видео на YouTube (оно удобно разбито на эпизоды).
Привет, сегодня я расскажу о том как оптимизировать размер и скорость сборки image и писать Dockerfile эффективнее.
Разрабатывая приложения и помещая их в контейнеры, можно заметить большой размер итогового image. Это сотни мегабайт либо даже гигабайты, которые необходимо спулить из какого-нибудь registry и запустить.
Например, размер image Spring Boot приложения в среднем составляет от двухсот мб.
![](https://habrastorage.org/getpro/habr/upload_files/642/258/599/642258599c1ffed38a7fbc7c6fa5dcb9.png)
Размер image Node.js приложения может составлять и более гигабайта.
Выбирайте правильный базовый image.
Обращаясь к Dockerhub мы часто видим множество тегов и не всегда понятно какой из них использовать. Множество популярных образов имеют тег slim
или alpine
. Эти теги обозначают версию, которая имеет минимальный вес image.
Если ваше приложение может работать с чуть ограниченной версией базового image, то используйте именно ее.
Например, image postgres:12.17-alpine
весит около 90 мб, а обычная версия postgres:12.17
уже около 140 мб. Экономия в 35% уже только на одном этапе.
![](https://habrastorage.org/getpro/habr/upload_files/f7b/8e5/05f/f7b8e505f2121d38f9ce3243ae3c690f.png)
![](https://habrastorage.org/getpro/habr/upload_files/41c/852/cd4/41c852cd44f45dd05dd26c2a03eedafe.png)
Файловая система и слои
Во втором видео, рассказывая об инструкциях Dockerfile я упомянул, что команды RUN
, COPY
и ADD
добавляют слои в итоговый image.
Эти инструкции работают с файловой системой Docker - Overlay FS. Докер работает не с целой файловой системой, а со слоями. Каждый инструкция, модифицирующая файловую систему, добавляет слой.
FROM ubuntu
RUN touch file1
RUN touch file2
RUN mkdir
RUN touch dir1/inner_file
На данном примере поверх базового image ubuntu выполняется 4 команды RUN
, в которой создаются файлы либо папки. Каждая из них добавляет новый слой.
Каждый следующий слой содержит результат предыдущего слоя. Слои неизменяемые, поэтому в этом примере они дозаписываются.
А что, если есть удаление или редактирование файлов?
FROM ubuntu
COPY package.deb
RUN dpkg -i package.deb
RUN rm package.deb
В этом примере на пятой строке удаляется файл, который был добавлен во втором слое на третьей строке. Однако слои неизменяемые, второй и третий слой содержат этот файл, но четвертый уже нет. Он помечается как удаленный.
При возможности старайтесь использовать меньше слоев. Одинаковые или однородные инструкции можно писать в одном слое, что сделает ваш image чуть меньше. Важно учитывать то, что слои могут кэшироваться и их стоит писать раздельно. То есть не нужно объединять слой, который не может быть закэширован (зачастую взаимодействие с локальной файловой системой), с подходящим для этого слоем.
Например, установка нескольких пакетов в Linux в одной инструкции RUN
сделает ваш Docker image чуть меньше.
FROM ubuntu
RUN apt-get update && apt-get install -y nginx
Удаление кэша
Устанавливая пакеты, в той же команде стоит очищать кэш, это может уменьшить размер image на несколько сотен мегабайт. К предыдущей команде дописываем удаление директории с кэшем.
FROM ubuntu
RUN apt-get update && apt-get install -y nginx && \
rm -f /var/lib/apt/lists/*
Так как слой представляет собой результат изменений одной инструкции, то если вы удалите ненужные файлы до конца этой команды, то они не попадут в слой - объединение команд в одну инструкцию помогают оптимизировать это.
Вы можете использовать экспериментальную функциональность docker build
- флаг –squash,
этот флаг позволяет сжать несколько слоев в один, что уменьшит размер image.
Также есть реализация подобной команды на Python, где вы можете сжать последние N слоев image.
.dockerignore
Если вы копируете файлы, то можете случайно скопировать ненужные файлы в контейнер. Кроме того, что это несет угрозу безопасности приложения, так и занимает место внутри файловой системы контейнера.
Создав .dockerignore
файл в директории с Dockerfile, вы можете ограничить build context - а это то, какие файлы попадут вовнутрь контейнера.
Копируйте в контейнер только то, без чего он не сможет нормально работать.
Кэширование
Используйте кэш при сборке image, это сократит время на сборку и запуск image. Docker хранит кэш каждого слоя на случай, если он пригодится в дальнейшем. Поэтому следует устанавливать зависимости до того, как вы используется команду COPY
или ADD
. Docker сможет закэшировать нужные слои с установленными зависимостями и переиспользовать их.
FROM ubuntu
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y vim net-tools dnsutils
COPY . .
FROM ubuntu
COPY . .
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y vim net-tools dnsutils
В этом примере первый Dockerfile лучше, чем второй, потому что позволяет Docker закэшировать слой с установкой зависимостей.
Многошаговая сборка
Dockerfile позволяет использовать несколько шагов для сборки конечного image. Следует использовать эту возможность, чтобы уменьшить размер итогового image.
Для работы вашего приложения зачастую требуется установить зависимости или пакеты. Это делается при помощи пакетных менеджеров как maven, npm или pip. Однако для работы приложения они не требуются, а требуется зачастую лишь среда исполнения языка.
Поэтому сборка итогового image будет состоять из двух шагов - установка зависимостей и копирование исполняемого файла.
На примере Java-приложения Dockerfile выглядит следующим образом.
FROM maven:3.8.5-openjdk-17 AS build
COPY /src /src
COPY pom.xml /
RUN -f /pom.xml clean package
FROM openjdk:17-jdk-slim
COPY --from=build /target/*.jar application.jar
EXPOSE 8083
ENTRYPOINT ["java", "-jar", "application.jar"]
Image собирается из двух шагов. На первом шаге собирается приложение и устанавливаются зависимости, а на втором шаге уже с другим базовым image это приложение запускается. Соответственно все те файлы, что были добавлены на первом шаге не попадут в конечный image.
А есть ли что-то еще?
Вы можете попробовать оптимизировать свой image и другими способами. Я рассказал не обо всех существующих вариантах, так как под каждое приложение есть отдельные способы оптимизации.
Я бы хотел выделить еще один совет, который поможет вам сократить размер image - оптимизируйте свое приложение. Уменьшая размер image вы можете бороться с проблемой, но не с ее причиной.
Подумайте о том эффективно ли ваше приложение использует зависимости, вдруг вы не нуждаетесь в каких-то и забыли их удалить?
Docker лишь позволяет вам запускать ваше приложение в любой среде и он гарантирует эффективную работу контейнера, а вы сами ответственны за то, что в этом контейнере будет запущено.