13g10n
Напишите мне
На главную

Как я исправил падающие сборки в GitHub Actions и уменьшил размер билда на 35%

Docker3 минуты

Представим с вами ситуацию: одним прекрасным днём кто-то на проекте работает над крутой фичей, добавляет в Dockerfile новую зависимость, но вместо закрытой задачи и довольных клиентов вы получаете бесконечно падающие билды в работающем ранее без нареканий GitHub Actions. "Кэши" — скажет кто-то и будет прав, но лишь отчасти.

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

Прежде всего, давайте взглянем на абстрактные исходные данные: самый обычный multistage (что абсолютно не важно, но агрессивно рекомендую) Dockerfile, какие-то переменные окружения, какие-то зависимости, назначение которых даже удобно прокомментировано:

Исходный Dockerfile
FROM ubuntu:XX.YY AS base

ENV FOOBAR=42

...

RUN apt update
RUN apt upgrade -y

RUN apt install -y libffi-dev libheif-dev libde265-dev  # HEIF/HEIC images
RUN apt install -y libwebp-dev  # Webp images
RUN apt install -y fonts-wqy-zenhei # Chinese fonts

...

Чтобы решить проблему с кэшами, а заодно и заставить наш билд похудеть, мы воспользуемся комбинацией из нескольких best practices, которые по какой-то причине (ха, угадай) редко упоминаются в разного рода курсах и туториалах на сомнительного качества сайтах.

Прежде всего (и это самая важная информация на этой странице!), решаем проблему с кэшами реформатировав обновление репозиториев и установку пакетов в одну инструкцию. Т.к. каждая строка в Dockerfile в конечном итоге экспортируется в "слой", что после используется (в том числе) как кэш для ускорения сборок, мы заставляем docker обновлять репозитории и устанавливать пакеты каждый раз, как мы добавляем новую зависимость, в то время как старый вариант будет использовать слои для сборки ДО строки с новым пакетом и тем самым не станет обновлять репозитории, что в долгосрочной перспективе вызовет ошибки.

Более того, теперь все наши зависимости упакованы в один слой, что потенциально снижает трафик и/или размер кэша.

Я также использую --no-install-recommends, чтобы не устанавливать лишние и потенциально ненужные мне зависимости, но этот шаг стоит выполнять только хорошо протестировав связанные с этими зависимостями области.

Вишенкой на торте нашего обновлённого Dockerfile'а станет удаление списков репозиториев, которые абсолютно нам не нужны в финальном билде, т.к. используются только для установки зависимостей.

Удаление репозиториев мы добавляем в конец нашего однострочника, чтобы ещё сильнее уменьшить в размерах наш промежуточный слой (и конечный билд, в данном случае).

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

Базовые образы обновляются относительно часто, поэтому вам достаточно пулить их с некоторой периодичностью (как правило с этим отлично справляются системы сборки, как тот же GitHub Actions), поэтому можно смело опускать инструкцию apt upgrade.

Взглянем на результат:

Улучшенный Dockerfile
FROM ubuntu:XX.YY AS base

ENV FOOBAR=42

...

RUN apt update \
    && apt install --no-install-recommends --assume-yes \ 
        libffi-dev libheif-dev libde265-dev \
        libwebp-dev \
        fonts-wqy-zenhei \
        ... \
    && rm -rf /var/lib/apt/lists/*

...

Результаты оптимизации будут разниться от проекта к проекту и всё это очень индивидуально, однако я получил -35% размера билда, что определённо отличный результат, особенно учитывая, что изначально мы просто фиксили сборку.

В примерах используется ubuntu и apt, но общие принципы применимы и к другим образам и другим пакетным менеджерам.

Я пофиксил, а ты подпишись на Telegram, ведь там тоже бывает интересно.

Docker
Performance