Dockerfile: как ускорить сборку и уменьшить образ
Не меняйте базовый образ на Alpine наугад: сначала переставьте инструкции так, чтобы изменения кода не сбрасывали кеш зависимостей. На примере Python 3.12 разберём слои, кеш BuildKit и способы сократить образ без потери воспроизводимости.

Если каждое изменение Python-кода заново устанавливает все зависимости, Dockerfile тратит время на работу, результат которой уже есть в кеше. А попытка «почистить» образ следующей инструкцией RUN может оставить удалённые файлы в предыдущем слое. Покажем, как перестроить сборку для Python 3.12 и BuildKit: кешировать установку пакетов отдельно от кода и уменьшать образ без случайных потерь.
Слои — это не просто строки Dockerfile
Dockerfile описывает последовательность инструкций. Часть из них создаёт или меняет файловую систему образа, часть задаёт его параметры. При сборке Docker может использовать кеш предыдущих шагов, если инструкция и нужные ей входные данные не изменились. Изменение файла, скопированного ранней инструкцией COPY, обычно заставляет пересобрать и следующие шаги.
Поэтому порядок важен. Если сначала скопировать весь проект, а потом установить зависимости, правка одного исходного файла может сбросить кеш установки. Если сначала скопировать только список зависимостей, а код — после установки, изменения приложения не затронут этот дорогой шаг.
Сноска. Инвалидация кеша — ситуация, когда Docker не может повторно использовать результат предыдущей инструкции и выполняет её заново. Кеш ускоряет сборку, но не заменяет проверку того, что собран новый образ.
Самый дешёвый способ ускорить сборку — не выполнять повторно шаг, результат которого не менялся.
Четыре приёма и цена каждого
| Приём | Что даёт | Чем платите |
|---|---|---|
| Сначала копировать файл зависимостей | Правки исходников не запускают установку пакетов заново | Нужно перечислять входные файлы установки отдельно |
| Кешировать данные пакетного менеджера через BuildKit | Повторная установка может использовать скачанные пакеты | Нужен BuildKit; кеш зависит от среды сборки и может быть очищен |
| Взять базовый образ slim | В базовом образе меньше лишних компонентов | Некоторым зависимостям понадобятся системные библиотеки или компилятор |
| Собирать в несколько этапов | Инструменты сборки можно не переносить в финальный образ | Dockerfile сложнее; нужно правильно перенести артефакты и runtime-библиотеки |
Кеш монтирования BuildKit отличается от обычного слоя: его содержимое доступно во время сборки, но не добавляется в файловую систему итогового образа. Это полезно для кеша pip. При этом сам кеш не гарантирован: на новом агенте CI или после очистки сборка скачает зависимости заново.
Рабочий Dockerfile для Python 3.12
Пример рассчитан на Dockerfile frontend 1.7 и сборщик BuildKit. В корне проекта должны лежать зафиксированный список зависимостей requirements.txt и пакет myapp с файлом __main__.py. Команда запуска предполагает, что приложение стартует как модуль python -m myapp.
# syntax=docker/dockerfile:1.7
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --disable-pip-version-check -r requirements.txt
COPY myapp/ ./myapp/
CMD ["python", "-m", "myapp"]Сборка из корня проекта: docker build -t myapp:dev .. Здесь список зависимостей копируется отдельно, поэтому правка файлов в myapp/ сохраняет кеш установки. Если изменился requirements.txt, установку нужно выполнить снова — это ожидаемо: изменился входной файл шага.
Для такого разделения нужен и аккуратный контекст сборки. Добавьте в .dockerignore как минимум локальное виртуальное окружение, кеши Python, каталог Git и файл с локальными переменными окружения. Иначе Docker отправит лишние файлы сборщику, увеличит контекст, а иногда случайно включит в образ данные разработчика. Не исключайте файл, который реально нужен приложению во время сборки.
Уменьшать образ — не значит удалять всё подряд
python:3.12-slim — разумная отправная точка, но не обещание, что приложение заработает без дополнительных системных библиотек. Если пакет содержит нативное расширение и для его сборки нужны компилятор или заголовочные файлы, проверьте, какие библиотеки нужны уже во время запуска. Удалить компилятор можно, а необходимую runtime-библиотеку — нет.
В такой ситуации подходит многоэтапная сборка: на одном этапе устанавливают инструменты и собирают зависимости, на другом оставляют только приложение и нужные артефакты. Не копируйте содержимое виртуального окружения между этапами вслепую: в нём могут быть исполняемые файлы, зависящие от библиотек, которых в финальном образе нет. После разделения проверьте запуск именно финального образа.
Не пытайтесь уменьшить образ командой удаления файлов в следующем слое. Если большой архив появился в одном слое, удаление в другом не отменит его присутствие в истории слоёв. Удалять временные файлы лучше в той же инструкции RUN, где они созданы. Но для Python-примера выше системные пакеты вообще не устанавливаются, поэтому добавлять такую очистку «на будущее» незачем.
Как понять, что оптимизация сработала
Сравнивайте сборки одного и того же проекта на одинаковой архитектуре и с одинаковыми зависимостями. Отдельно смотрите на время повторной сборки после изменения исходника и после изменения списка зависимостей: первое должно обходиться без повторной установки пакетов, второе — запускать её.
docker history myapp:devпоказывает, какие инструкции сформировали слои и где появился большой вклад.docker image inspect --format='{{.Size}}' myapp:devвыводит размер локального образа в байтах. Сравнивайте значения до и после на одной платформе.- Проверьте, что в финальном образе нет исходников, кешей, локального виртуального окружения и секретов, которые не нужны приложению.
- Запустите контейнер в той же конфигурации, в которой его планируете выпускать: сборка успешна не только тогда, когда Dockerfile дошёл до конца.
Размер локального образа не равен размеру скачивания из реестра: при передаче слои сжимаются, а часть слоёв у получателя может уже быть. Поэтому «стало меньше на диске» и «деплой стал быстрее» — не всегда одно и то же.
Когда что выбирать и что ломается
Начните с раздельного COPY файла зависимостей и исходников: это обычно даёт самый понятный выигрыш без усложнения структуры. Кеш-монтирование добавляйте, если зависимости часто переустанавливаются и сборщик сохраняет кеш между запусками. Slim проверяйте интеграционным запуском. Многоэтапную сборку вводите, когда инструменты компиляции заметно утяжеляют финальный образ или не должны в нём оставаться.
Типичные сбои связаны не с кешем, а с ожиданиями от него и окружения:
- Зависимости не переустановились после изменения файла: проверьте, что копируете именно тот файл, который указан в RUN.
- После перехода на slim модуль не импортируется: выясните, какой системной библиотеке он соответствует, и добавьте нужную runtime-зависимость.
- На CI сборка медленная, хотя локально быстрая: среда CI могла не сохранить BuildKit-кеш между запусками.
- Контейнер собирается, но завершается сразу после старта: проверьте наличие
myapp/__main__.pyи работоспособность команды запуска.
Подробности о кешировании, кеш-монтированиях и многоэтапных сборках есть в документации Docker по оптимизации кеша и в разделе о многоэтапных сборках.
Что сделать дальше
Сохраните текущий Dockerfile и запишите два показателя: время повторной сборки после правки исходника и размер образа. Затем измените порядок COPY, соберите образ снова и проверьте, что установка зависимостей взялась из кеша. После этого отдельно оцените slim или многоэтапную сборку — и убедитесь, что контейнер запускается в чистом окружении. Для воспроизводимости зафиксируйте версии зависимостей, а используемый тег базового образа сверяйте с политикой обновлений проекта.
Антиблок-аналитик 115-ФЗ: Анализ выписок, поиск рисков блокировки счёта, инструкции по безопасным транзакциям.
Спросить за 41 ₽Источники: Docker documentation: Optimize cache usage in builds, Docker documentation: Multi-stage builds
Частые вопросы
Каждая инструкция Dockerfile создаёт слой?
Кеш BuildKit увеличивает размер итогового образа?
Стоит ли всегда заменять slim на Alpine ради меньшего размера?
Почему размер образа на диске не совпадает с размером загрузки?
Что делать, если у меня другая версия Docker или Dockerfile?
Безопасно ли переносить виртуальное окружение между этапами сборки в проде?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.