Привет.

Делюсь лайфхаком по уменьшению размеров 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.

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


  1. Alexufo
    17.10.2024 22:30

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


    1. WondeRu Автор
      17.10.2024 22:30

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


      1. Alexufo
        17.10.2024 22:30

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


        1. whocoulditbe
          17.10.2024 22:30

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


        1. WondeRu Автор
          17.10.2024 22:30

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


        1. WondeRu Автор
          17.10.2024 22:30

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


  1. seyko2
    17.10.2024 22:30

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


    1. randomsimplenumber
      17.10.2024 22:30

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


  1. vitaly_il1
    17.10.2024 22:30

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


    1. WondeRu Автор
      17.10.2024 22:30

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