Эта публикация - текстовый вариант и сценарий для видео на YouTube (оно удобно разбито на эпизоды).

Привет, сегодня я расскажу о том как оптимизировать размер и скорость сборки image и писать Dockerfile эффективнее.

Разрабатывая приложения и помещая их в контейнеры, можно заметить большой размер итогового image. Это сотни мегабайт либо даже гигабайты, которые необходимо спулить из какого-нибудь registry и запустить.

Например, размер image Spring Boot приложения в среднем составляет от двухсот мб.

Размер image Node.js приложения может составлять и более гигабайта.

Выбирайте правильный базовый image.

Обращаясь к Dockerhub мы часто видим множество тегов и не всегда понятно какой из них использовать. Множество популярных образов имеют тег slim или alpine. Эти теги обозначают версию, которая имеет минимальный вес image.

Если ваше приложение может работать с чуть ограниченной версией базового image, то используйте именно ее.

Например, image postgres:12.17-alpine весит около 90 мб, а обычная версия postgres:12.17 уже около 140 мб. Экономия в 35% уже только на одном этапе.

Файловая система и слои

Во втором видео, рассказывая об инструкциях 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 лишь позволяет вам запускать ваше приложение в любой среде и он гарантирует эффективную работу контейнера, а вы сами ответственны за то, что в этом контейнере будет запущено.

Комментарии (8)


  1. ky0
    01.04.2024 04:51
    +2

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


    1. igolikov
      01.04.2024 04:51

      А каким именно образом, из статьи следует, что это ускорит сборку докер образа за счёт того, что пересобирается только один слой, но на размер образа влиять не должно


      1. ky0
        01.04.2024 04:51

        Да, вы правы - но я как-то подсознательно имел в виду, что приложение у нас не одно, а следовательно - сэкономить можно на одинаковых "нижних спринговых" слоях.


        1. igolikov
          01.04.2024 04:51

          да, об этом не подумал, если еще вместо загрузки образов всяких сервисов с докер-хаба собрать все самому на базе одного базового образа - можно наверное добиться существенной экономии


  1. DarkHost
    01.04.2024 04:51
    +4

    Старайтесь использовать меньше слоев. Одинаковые или однородные инструкции можно писать в одном слое, что сделает ваш image чуть меньше.

    Неверно! Это ошибка, которую все друг у друга копируют, не понимая смысла. Одна из задач слоев - кэширование результата этапа для ускорения сборки. Это требует вдумчивого подхода, особенно для образов, которые долго собираются.

    А насколько образ станет меньше, при уменьшении кол-ва слоев, вы проверяли? ;)


  1. Evgenym
    01.04.2024 04:51

    Ох уж эти слои и святой Alpine. Каждая первая статья про это.


  1. Johan_Palych
    01.04.2024 04:51

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

    # Install required packages to run
    RUN     DEBIAN_FRONTEND=noninteractive apt-get update \
        &&  DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --fix-missing --no-install-recommends nginx \
        &&  apt-get clean -y && apt-get autoremove -y && apt-get autoclean -y && rm -r /var/lib/apt/lists/*


  1. nnstepan
    01.04.2024 04:51

    А почему упомянут только overlay, ведь есть еще btrfs, zfs, vfs