Как уменьшить время сборки и размер Docker-образов?
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
- Удаление кэша 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
- Использование .dockerignore. Этот файл декларирует, какие файлы/папки не попадут в building context → в docker image, и итоговый образ будет легче.
- Подчищаем системные кэши. Помимо python-зависимостей, есть ещё и системные. Cтоит использовать один слой для их установки и очистки, тогда он будет кэшироваться, а лишние данные не попадут в итоговый образ:
RUN apt-get update && apt-get install -y --no-install-recommends \\
build-essential \\
&& rm -rf /var/lib/apt/lists/*
- Выбор базового образа. Старайтесь использовать облегчённые базовые образы, например:
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"]
В примере выше мы:
- Уменьшили образ. gcc/make и прочие инструменты остаются в builder, в final image попадают только установленные пакеты и код.
- Выдали чистый итоговый image. В нашем final нет ничего лишнего, случайные инструменты не выкатываются на прод.
- Используем разные образы для 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 под все три окружения.

