Привет.

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

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