Назад
840

Как уменьшить время сборки и размер Docker-образов?

840

Docker — стандарт для контейнеризации при разработке и эксплуатации современных приложений. С ростом проекта разработчики сталкиваются с такими болями, как увеличение времени сборки Docker-образов и их размера. В этой статье мы предлагаем рецепты, с помощью которых можно сделать docker-образы меньше, а сборки — быстрее 😉

Ускорение сборки docker image

Наивный Dockerfile для web-приложения на Python выглядит так:

FROM python:3.12
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "app.main:app"]

При изменении кода и пересборке образа установка зависимостей запустится заново. Это увеличит время сборки и добавит дискомфорта в разработку 🙁

Дело в том, что команда COPY изменит слой в сборке, следовательно, кэш следующих слоёв станет неактуальным, и их нужно будет собрать заново.

Есть решение — реструктурировать Dockerfile: прописать редко меняющиеся компоненты в начало Dockerfile, а часто меняющееся — в конец:

FROM python:3.12
WORKDIR /app

# 1) Сначала делаем этап с зависимостями, они, очевидно, будут меняться реже
# Здесь же могут быть различные apt install/ apt get

COPY requirements.txt ./
RUN pip install -r requirements.txt

# 2) А затем уже пойдет часть с нашим кодом, которая будет меняться чаще

COPY . .

CMD ["uvicorn", "app.main:app", "--host=0.0.0.0", "--port=8000"]

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

Уменьшение размера docker image

  1. Удаление кэша pip. При установке зависимостей через pip создаётся кэш, который не нужен в итоговом образе. Для установки зависимостей без кэша надо добавить флаг --no-cache-dir:
RUN pip install --no-cache-dir -r requirements.txt

Например, с таким requirements без сохранения кэша pip можно сэкономить пару ГБ:

torch==2.3.1
fastapi==0.115.0
uvicorn[standard]==0.30.0
psycopg2-binary==2.9.9
lxml==5.2.2
pillow==10.4.0
  1. Использование .dockerignore. Этот файл декларирует, какие файлы/папки не попадут в building context → в docker image, и итоговый образ будет легче.
  1. Подчищаем системные кэши. Помимо python-зависимостей, есть ещё и системные. Cтоит использовать один слой для их установки и очистки, тогда он будет кэшироваться, а лишние данные не попадут в итоговый образ:

RUN apt-get update && apt-get install -y --no-install-recommends \\
      build-essential \\
  && rm -rf /var/lib/apt/lists/*
  1. Выбор базового образа. Старайтесь использовать облегчённые базовые образы, например:
  • python:3.12-slim вместо полного python:3.12;
  • alpine — крошечный базовый образ, но есть проблемы с бинарными зависимостями.

Кроме того, лучше использовать точный tag, а не :latest. Детерминизм и повторяемость сборок → меньше сюрпризов при сборке.

Замечание

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

Multi-stage сборка

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

Популярный сценарий использования multi-stage сборки — в requirements есть пакеты, требующие компиляции (pillow, psycopg2, cryptography), но сам компилятор в финальном образе не нужен.

# Первый этап с builder
FROM python:3.12 AS builder
WORKDIR /w
COPY requirements.txt .
# На этом шаге все наши зависимости мы превращаем в колёса (wheels)
RUN pip wheel --wheel-dir /wheels -r requirements.txt

# Второй этап с финальным образом
FROM python:3.12-slim AS final
WORKDIR /app
# Переносим собранные колёса из builder
COPY --from=builder /wheels /wheels
# Ставим зависимости из предыдущего этапе (без сети и кэша в слое),
# Затем удаляем /wheels, чтобы не тащить их в финальный образ
COPY requirements.txt .
RUN pip install --no-index --find-links=/wheels --no-cache-dir -r requirements.txt \\
    && rm -rf /wheels
COPY . .
CMD ["uvicorn", "app.main:app"]

В примере выше мы:

  1. Уменьшили образ. gcc/make и прочие инструменты остаются в builder, в final image попадают только установленные пакеты и код.
  2. Выдали чистый итоговый image. В нашем final нет ничего лишнего, случайные инструменты не выкатываются на прод.
  3. Используем разные образы для builder и final. Сборка сделана на тяжёлом образе python3.12 (нужной для установки зависимостей), а запуск — уже на облегчённой slim-версии.

Другой сценарий использования multi-stage — создание единого Dockerfile для разных окружений в проекте:

  • прод — c минимальным набором библиотек для корректной работы приложения;
  • тест — с pytest и вспомогательными библиотеками;
  • дев — с профайлерами и утилитами.

Иметь отдельный Dockerfile под каждое окружение — не оптимально. Это дублирование кода, кэш не переиспользуется, и сборка становится медленной.

Используя multi-stage, мы описываем первый этап один раз, а затем для каждого окружения прописываем нужные дополнения:

FROM python:3.12 AS builder
WORKDIR /w

COPY requirements.txt .
COPY requirements-dev.txt .
COPY requirements-test.txt .

# Собираем все наши зависимости в wheels
RUN pip wheel --wheel-dir /wheels -r requirements.txt \\
 && pip wheel --wheel-dir /wheels -r requirements-dev.txt \\
 && pip wheel --wheel-dir /wheels -r requirements-test.txt

# prod
FROM python:3.12-slim AS prod
WORKDIR /app
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN pip install --no-index --find-links=/wheels --no-cache-dir -r requirements.txt \\
    && rm -rf /wheels
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

# dev
FROM prod AS dev
COPY --from=builder /wheels /wheels
COPY requirements-dev.txt .
RUN pip install --no-index --find-links=/wheels --no-cache-dir -r requirements-dev.txt \\
    && rm -rf /wheels
    
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

# test
FROM prod AS test
COPY --from=builder /wheels /wheels
COPY requirements-test.txt .
RUN pip install --no-index --find-links=/wheels --no-cache-dir -r requirements-test.txt \\
    && rm -rf /wheels
CMD ["pytest", "tests"]

Затем собираем только необходимое следующими командами:

  • docker build —target prod -t myapp:prod .
  • docker build —target dev -t myapp:dev .
  • docker build —target test -t myapp:test .

Таким образом, с помощью multi-stage сборки можно обойтись одним Dockefile под все три окружения.

Старт — 28 августа
Деплой DL-сервисов

Научитесь создавать и деплоить DL-сервисы за 4 месяца. Наведите порядок в репозиториях, внедрите лучшие практики и повысьте свою ценность на рынке

0/0

Телеграм-канал

DeepSchool

Короткие посты по теории ML/DL, полезные
библиотеки и фреймворки, вопросы с собеседований
и советы, которые помогут в работе

Открыть Телеграм

Увидели ошибку?

Напишите нам в Telegram!