Главное из статьи
- Используйте slim-образы вместо полных версий Python.
- Внедряйте многоэтапную сборку (multi-stage builds), чтобы отсечь лишний «мусор» после компиляции.
- Соблюдайте правильный порядок слоев: зависимости ставим раньше, чем копируем код.
- Отключайте кеши pip и poetry в финальном образе.
- Настраивайте переменные окружения для корректной работы логов и Python-байткода.
Анатомия слоев: почему ваш образ такой тяжелый?
Чтобы оптимизировать Docker, нужно понять, как он работает. Образ - это стопка слоев. Каждый раз, когда вы пишете RUN, COPY или ADD, Docker создает новый слой. Если вы установили пакет, а в следующей строке удалили его, размер образа не уменьшится - удаление просто создаст новый слой, «закрывающий» старый. Вес предыдущего слоя всё равно останется в истории.
Особенно много лишнего прилетает с базовых образов. Стандартный образ Python содержит полноценную ОС Debian со всеми утилитами, которые в 99% случаев в продакшене не нужны. Переход на python-slim версию может сэкономить вам сотни мегабайтов сразу. Например, для свежего Python 3.13 рекомендую использовать python:3.13-slim-bookworm. Здесь "slim" означает минимальный набор пакетов, а "bookworm" - версию Debian.
Многоэтапная сборка: разделяй и властвуй
В ML-проектах часто нужны компиляторы gcc или libc-dev для установки нативных расширений библиотек. Но нужны ли они вам в работающем приложении? Конечно, нет. Здесь на помощь приходит multi-stage build.
Смысл в том, чтобы создать два этапа: builder (где мы всё компилируем и собираем) и runtime (где только запускаем код). В первом этапе мы устанавливаем все тяжелые инструменты, собираем зависимости в wheel-файлы, а во второй переносим только готовый результат.
| Критерий | Обычный Dockerfile | Multi-stage build |
|---|---|---|
| Инструменты сборки (gcc, make) | Остаются в образе | Удаляются вместе с первым этапом |
| Кеши менеджеров пакетов | Занимают место | Не попадают в финальный слой |
| Итоговый размер | Большой (ГБ) | Минимальный (МБ) |
| Сложность написания | Низкая | Средняя |
Правильный порядок инструкций и магия кеша
Docker кеширует каждый слой. Если вы изменили одну строчку в коде, Docker инвалидирует кеш для этого слоя и всех последующих. Если вы копируете весь проект COPY . . в начале файла, а затем запускаете установку зависимостей, то при любом изменении кода Docker будет переустанавливать все библиотеки заново. Это ужасно замедляет сборку.
Правильный паттерн: сначала копируем только файлы определений зависимостей (requirements.txt или pyproject.toml и poetry.lock), устанавливаем их, и только потом копируем остальной код. Так, если вы правите функцию в main.py, слой с установкой тяжелых ML-библиотек останется нетронутым и возьмется из кеша за доли секунды.
Оптимизируем зависимости: Pip и Poetry
Если вы используете Poetry, помните, что он по умолчанию создает виртуальные окружения. Внутри контейнера это лишняя вложенность. Используйте команду poetry config virtualenvs.create false, чтобы ставить пакеты прямо в систему. Это упрощает структуру и немного облегчает образ.
Не забывайте про флаги очистки. При использовании pip всегда добавляйте --no-cache-dir. Это предотвратит создание кеша загрузок внутри образа. Если используете Poetry, очистите кеш после установки: poetry cache clear --all pypi.
Для системных пакетов в Debian используйте цепочку команд в одной инструкции RUN, чтобы не плодить слои, и обязательно удаляйте списки пакетов: apt-get clean && rm -rf /var/lib/apt/lists/*. Это «золотой стандарт» очистки.
Тонкая настройка Python для контейнеров
Чтобы Python вел себя предсказуемо в Docker, добавьте несколько переменных окружения в ваш Dockerfile. Это не уменьшит размер, но спасет от проблем с логами и производительностью:
PYTHONUNBUFFERED=1- отключает буферизацию вывода. Без этого ваши логи в Kubernetes могут появляться с огромной задержкой или вообще пропасть при краше.PYTHONDONTWRITEBYTECODE=1- запрещает создавать.pycфайлы. В контейнере они просто занимают место и не дают никакого реального профита.LANG=C.UTF-8- гарантирует, что Unicode будет обрабатываться корректно, что критично для ML-моделей, работающих с разными языками.
Также хорошим тоном считается создание не-root пользователя. Запуск контейнера от root - это дыра в безопасности. Создайте пользователя через useradd и переключитесь на него с помощью инструкции USER в конце файла.
Как проверить, что оптимизация работает?
Не гадайте на кофейной гуще - используйте инструменты анализа. Самый крутой из них - dive. Эта утилита позволяет буквально «заглянуть» внумь каждого слоя и увидеть, какие файлы были добавлены, изменены или удалены. Вы сразу заметите, если какой-нибудь временный лог-файл или кеш случайно попал в финальный образ.
В Docker Desktop есть вкладка Images, где виден размер слоев. Если вы видите слой в 1 ГБ, который появляется после установки какой-то мелкой утилиты - значит, вы где-то ошиблись в цепочке RUN или забыли очистить кеш.
Почему первая сборка multi-stage образа идет так долго?
Потому что Docker нужно скачать базовый образ, установить все инструменты сборки (компиляторы, зависимости) и собрать wheel-файлы. Однако все последующие сборки будут молниеносными, так как Docker закеширует этап сборки, и вам придется ждать только копирования вашего измененного кода.
Стоит ли использовать Alpine Linux для ML-приложений?
Скорее нет, чем да. Alpine использует библиотеку musl вместо glibc. Большинство ML-библиотек (numpy, pandas, pytorch) скомпилированы под glibc. Попытка поставить их на Alpine часто приводит к тому, что пакеты приходится компилировать из исходников прямо в контейнере, что занимает часы и в итоге может создать образ большего размера, чем slim-версия Debian.
Что такое .dockerignore и зачем он нужен?
Это файл, который говорит Docker, какие папки и файлы не нужно отправлять в контекст сборки. Если вы не добавите туда .git, __pycache__, .venv и локальные данные моделей, Docker будет копировать их в демон сборки каждый раз, что замедлит процесс и может случайно раздуть образ.
Как правильно копировать пакеты из builder-этапа?
Лучше всего копировать конкретные пути. Например: COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages и бинарники из /usr/local/bin/. Это гарантирует, что в runtime-образ попадут только установленные библиотеки, но не инструменты сборки.
Помогает ли флаг --no-cache-dir в многоэтапной сборке?
Да, помогает. Даже в builder-этапе кеши pip занимают место. Хотя builder-этап в итоге удаляется, использование этого флага ускоряет передачу данных и делает процесс более чистым. В runtime-этапе он обязателен, если вы вдруг решите что-то доустановить.
Следующие шаги и решение проблем
Если после всех манипуляций образ всё равно кажется вам слишком большим, проверьте следующие моменты:
- Веса моделей: Не запекайте веса нейросетей прямо в образ. Используйте S3-хранилища или примонтированные Volume в Kubernetes. Это позволит обновлять модель без пересборки всего контейнера.
- Зависимости: Проверьте
pyproject.toml. Возможно, у вас установлены тяжелые библиотеки в секции основных зависимостей, хотя они нужны только для тестов. Перенесите их вdev-dependencies. - Слои: Объединяйте команды
apt-get updateиapt-get installв одну строку через&&. Это классическая ошибка, когда update создает отдельный слой, который никогда не меняется, но занимает место.