Mount — ещё один способ уменьшения размера Docker-образа

Привет.

Делюсь лайфхаком по уменьшению размеров Docker-образов. Как-то нам попалась на поддержку и развитие CRM-система, написанная на Ruby. Пришли со словами: предыдущий разработчик не передал исходный код, но систему нужно развивать. Я уверен, что по условиям контракта передавали исходный код, но заказчики всегда относятся попустительски: им присылают архив на почту, а они потом стирают старое барахло, чтобы ящик почистить.

Так вот, зайдя на продакшен-сервер, я нашел развернутую платформу, да ещё и с .git папочкой. Ура, у меня были исходники с историей (она потом мне ни разу не понадобилась). Загрузил в нашу репу исходники, поизучал. В ходе контракта нужно было изменить деплой с rsync на контейнеризацию и перетащить все на Alt Linux (или Astra, уже не помню).

Обновили Ruby-пакеты (gems), обновили под них код и написали Dockerfile. Первая сборка была удручающей: образ в 2Гб. Это нормальный размер, если ты собираешь образ с Torch и другой ML-штуковиной, но CRM - нет. В результате дальнейших действий, удалось сократить размер образа до 200Мб.

Если кратко, сделали банальные действия, чтобы сократить размеры образа:

  1. Multistage.

  2. Избавление от ненужный gem-ов.

  3. Неизменение прав на файлы. Если вы вздумаете выполнить команду RUN chmod +x /app/something, то создастся новый слой с копией этого something, но с другими разрешениями. Это актуально для больших папок и файлов.

  4. Установка пакетов ОС и удаление кэша пакетов в этом же слое.

А также были проведены чуть менее очевидные операции:

  1. С помощью dive (крайне рекомендую этот tool для того, чтобы посмотреть, что находится в каждом Docker-слое) выяснилось, что примерно 1 Гб занимает gem-пакет для генерации PDF. При чуть более пристальном взляде оказалось, что ребята загружают бинарники для 10 различных ОС и, даже, для Windows, а потом используют только один бинарник. Естественно, в том же слое, где ставился этот пакет, мы подчистили с помощью rm -rf этот ненужный хлам. Еще в пакеты любят пихать документацию и тесты, их тоже можно и нужно удалять, если вы настолько же параноидальны по поводу размеров.

  2. RUN mount - это способ монтирования данных из других stage в multistage без их копирования, а также выполнения действий в одном слое RUN. Если хотите, посмотрите официальную непонятную документацию https://docs.docker.com/reference/dockerfile/#run---mounttypebind, но лучше посмотрите пример ниже.

Mount

Обычно, в первом stage мы делаем компиляцию, а потом результат копируем в результирующий stage.

COPY --from=builder /app/public ./public

Периодически возникает потребность поставить пакеты в финальном слое, но инсталляторы для этого нам не нужны. Давайте приведу пример Dockerfile простого Python-проекта, тот проект на Ruby показывать не могу:

# Stage 1: Build the wheels
FROM python:3.11-alpine AS builder

ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

# Set the working directory
WORKDIR /app

# Copy the requirements file into the container
COPY requirements.txt .

# Install the build dependencies
RUN pip install --no-cache-dir wheel

# Install the dependencies and build the wheels
RUN pip wheel --no-cache-dir --wheel-dir /app/wheels -r requirements.txt

# Stage 2: Create the final image
FROM python:3.11-alpine

ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

# Create a non-root user and group
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Set the working directory
WORKDIR /app

# Install the wheels
RUN --mount=type=bind,from=builder,source=/app/wheels,target=/wheels pip install --no-cache-dir --no-index --find-links=/wheels /wheels/*

# Set file permissions and ownership
RUN chown -R appuser:appgroup /app

# Switch to the non-root user
USER appuser

# Copy the rest of the application code into the container
COPY . .

# Specify the command to run the application
CMD ["python", "app.py"]

Самое интересное здесь:

RUN --mount=type=bind,from=builder,source=/app/wheels,target=/wheels pip install --no-cache-dir --no-index --find-links=/wheels /wheels/*

Мы не копируем папку /app/wheels в /wheels, а используем как временную. Это позволяет выполнить установку пакетов и сразу же забыть о существовании папки . Конечно, можно копировать уже установленные пакеты из python3.11/site-packages, а не wheels, но это уже попахивает костылём. Затем проверяем с помощью dive - да! папки /wheels не существует ни в одном слое! Только на этой простой команде экономия сразу в 100Мб в моем случае.

Надеюсь, данные способы помогут вам в сокращении размеров Docker-образов.

PS. Это будет моя 10-я (я удалил несколько, чтобы не привлекать внимание санитаров) статья здесь, поэтому, в честь юбилея, если вам будет интересно почитать про работу CTO в небольшой компании, подписывайтесь на мой новый Telegram-канал https://t.me/cto_podsekin.

@WondeRu
17.10.2024 15:40 UTC
Первоисточник

Комментарии

@Alexufo
17.10.2024 22:30 UTC
0

Как перетаскивать бинари питона собранные, чтобы не тащить зависимости для сборки?

@WondeRu
18.10.2024 05:40 UTC
0

Не совсем понял, пакеты питона не компилируются в бинарники, в редких случаях содержат скомпилированные библиотеки. Если совсем хочется без пакетов, то нужно смотреть в сторону cyphon. Есть еще .pyc файлы - байткод питона, можно их попробовать потаскать в финальный образ, но я не пробовал и не понимаю, какие могут быть проблемы

18.10.2024 07:25 UTC
0

Нужно собрать питон из исходников, для сборки нужна куча зависимостей. Если После сборки не понятно, как перетащить собранный питон, разбросанный своим телом по системным папкам. Разве что делать пакет deb или rpm но вот с ними возиться не очень хочется, хочется простым копированием файлов обойтись

18.10.2024 08:28 UTC
+1

установите собранное в отдельную директорию? https://docs.python.org/3/using/configure.html#install-options

18.10.2024 10:33 UTC
+1

почему не хотите готовый образ использовать? python:3.12-slim-bookworm - этот без всякой шелухи.

18.10.2024 10:40 UTC
+1

Но если у вас базовый образ, не приведи господь, какая-нибудь Астра, то идем в исходники образа python и копируем скрипты as is https://github.com/docker-library/python/blob/master/3.12/slim-bookworm/Dockerfile

24.10.2024 13:00 UTC
0

Почему же, есть nuitka. Это не упаковщик как фриз или пайинстайлер.

@seyko2
18.10.2024 04:45 UTC
0

Сделать из содержимого подкаталога файл squash_fs.img несложно. А вот как подсунуть его в docker? Ведь слои то в нем и состоят из таких образов (нового вроде ничего не придумали), смонтированных с помощью overlayfs (в OpenVx всё было попонятнее как то)

@randomsimplenumber
18.10.2024 05:16 UTC
0

А зачем? Копирование даст тот же результат, но без squashfs.

19.10.2024 04:04 UTC
0

Разница в объёме занимаемого результатом дискового пространства? Не? Ну если только результат автоматом превращается в squshfs образ или используется сжатие на уровне файловой системы (пока докер не изучал, а для openvz образы сжимал с помощью suashfs и монтировал перед запуском)

19.10.2024 08:15 UTC
0

Разница в объёме занимаемого результатом дискового пространства? Не?

Ну если так очень нужно, не вижу препятствий втащить образ squashfs внутрь docker image и смонтировать через loop.

@vitaly_il1
18.10.2024 08:51 UTC
+1

Спасибо!
Я не уверен что понял чем mount лучше/удобнее чем "стандартное" копирование файлов при multi-stage build.

@WondeRu
18.10.2024 10:35 UTC
+1

когда копируете что-то постоянное, чем будете пользоваться в рантайме, то COPY --from,а когда нужно в рантайм контейнере что-то временное получить из предыдущего этапа, то RUN --mount

18.10.2024 12:02 UTC
+1

спасибо, дошло!

@WerySkok
19.10.2024 07:01 UTC
+1

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

https://docs.docker.com/guides/rust/develop/

@DieSlogan
22.10.2024 10:42 UTC
+1

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

https://docs.docker.com/reference/dockerfile/#onbuild