Если измерить (например, через профайлер), на что уходит это время, окажется, что часть времени занимает импорт модулей (я про проблему импортов в Python даже делал доклад и писал посты), а не инициализация приложения.
Причина в том, что в образе нет байткода после сборки. Официальный образ python удаляет файлы .pyc при сборке, чтобы уменьшить размер, поэтому стандартная библиотека (stdlib) лежит в нем без байткода.
Poetry и uv по умолчанию не компилируют зависимости при установке. Часто в Dockerfile еще задан
PYTHONDONTWRITEBYTECODE=1 и приложение работает не от root. Тогда Python не может сохранить скомпилированные модули, и каждый процесс при каждом старте заново компилирует их из исходников. В сервисе среднего размера это несколько тысяч модулей.Сохранять байткод во время работы бесполезно: каждый новый контейнер стартует с файловой системы образа. Компилировать нужно при сборке, для этого достаточно двух команд в Dockerfile.
В базовом этапе, от root, компилируем стандартную библиотеку:
RUN python -m compileall -q -j0 "$(python -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')"
В финальном этапе компилируем приложение и зависимости:
RUN python -m compileall -q -j0 app/ .venv/
В моих замерах время импорта приложения сократилось вдвое, под в кластере стал готов вдвое быстрее. За первые 90 секунд после старта под израсходовал на 45% меньше CPU, память после старта снизилась на 6%. В установившемся режиме потребление не меняется: байткод влияет только на импорт. Образ вырастает примерно на 4%.
Изменение безопасно. Python сравнивает .pyc с исходным файлом и не использует устаревший байткод, поэтому ошибка в этом шаге не приведёт к выполнению старого кода. Документация uv рекомендует компилировать байткод в продовых образах. У Poetry для этого есть флаг poetry install --compile. pip компилирует байткод по умолчанию.
Одно ограничение: compileall возвращает ошибку, если в каком-нибудь пакете окажется файл с синтаксисом, который текущая версия Python не разбирает. Сборка тогда упадет. pip и poetry install --compile такие файлы пропускают.
Проверить свой образ можно так:
docker run --rm --entrypoint sh <image> -c \
'find / -name "*.py" -path "*python3*" | wc -l; find / -name "*.pyc" -path "*python3*" | wc -l'
Первое число - количество исходных файлов, второе - файлов байткода. Если второе намного меньше первого, образ компилирует модули при каждом старте.
Изменение ускоряет запуск контейнера и уменьшает всплеск CPU при старте. Поэтому при настройке requests и limits в Kubernetes можно закладывать меньше запаса на старт приложения.