Mount — ещё один способ уменьшения размера Docker-образа
Привет.
Делюсь лайфхаком по уменьшению размеров Docker-образов. Как-то нам попалась на поддержку и развитие CRM-система, написанная на Ruby. Пришли со словами: предыдущий разработчик не передал исходный код, но систему нужно развивать. Я уверен, что по условиям контракта передавали исходный код, но заказчики всегда относятся попустительски: им присылают архив на почту, а они потом стирают старое барахло, чтобы ящик почистить.
Так вот, зайдя на продакшен-сервер, я нашел развернутую платформу, да ещё и с .git папочкой. Ура, у меня были исходники с историей (она потом мне ни разу не понадобилась). Загрузил в нашу репу исходники, поизучал. В ходе контракта нужно было изменить деплой с rsync
на контейнеризацию и перетащить все на Alt Linux (или Astra, уже не помню).
Обновили Ruby-пакеты (gems), обновили под них код и написали Dockerfile. Первая сборка была удручающей: образ в 2Гб. Это нормальный размер, если ты собираешь образ с Torch и другой ML-штуковиной, но CRM - нет. В результате дальнейших действий, удалось сократить размер образа до 200Мб.
Если кратко, сделали банальные действия, чтобы сократить размеры образа:
Multistage.
Избавление от ненужный gem-ов.
Неизменение прав на файлы. Если вы вздумаете выполнить команду
RUN chmod +x /app/something
, то создастся новый слой с копией этого something, но с другими разрешениями. Это актуально для больших папок и файлов.Установка пакетов ОС и удаление кэша пакетов в этом же слое.
А также были проведены чуть менее очевидные операции:
С помощью dive (крайне рекомендую этот tool для того, чтобы посмотреть, что находится в каждом Docker-слое) выяснилось, что примерно 1 Гб занимает gem-пакет для генерации PDF. При чуть более пристальном взляде оказалось, что ребята загружают бинарники для 10 различных ОС и, даже, для Windows, а потом используют только один бинарник. Естественно, в том же слое, где ставился этот пакет, мы подчистили с помощью
rm -rf
этот ненужный хлам. Еще в пакеты любят пихать документацию и тесты, их тоже можно и нужно удалять, если вы настолько же параноидальны по поводу размеров.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.