Что такое multi-stage сборка в Docker
Что проверяют
Понимаете ли вы, что образ для сборки и образ для запуска это разные вещи, и умеете ли объяснить, из чего складывается размер и поверхность атаки контейнера. Для Python-разработчика вопрос практический: зависимости с расширениями ставятся компилятором, а в проде компилятор не нужен.
Как отвечать
Multi-stage это несколько инструкций FROM в одном Dockerfile. Каждая начинает новый этап со своей файловой системой, этапы именуются через AS, а в финальный образ попадает только то, что забрали через COPY --from. Остальное (исходники, компиляторы, заголовочные файлы, кеш пакетного менеджера, ключ к приватному репозиторию) остаётся в промежуточных слоях и наружу не едет.
FROM python:slim AS builder
RUN python -m venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:slim
COPY --from=builder /opt/venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH
COPY . .
USER 1000
CMD ["python", "-m", "app"]Полезно добавить, что этапы это ещё и способ разложить пайплайн: отдельный этап под тесты и линтеры, флаг --target для сборки нужного, а BuildKit пропускает этапы, от которых финальный не зависит, и собирает независимые параллельно.
Где спотыкаются
Копируют каталог пакетов из полного образа в урезанный. Пакеты с C-расширениями собраны под библиотеки сборочного образа, и в слиме их нет: контейнер падает на импорте. Переносить надо виртуальное окружение целиком или заранее собранные колёса, и обязательно на одинаковой базе и одной минорной версии интерпретатора.
Считают, что RUN rm удаляет секрет. Внутри одного этапа слой уже записан, и файл достаётся из истории образа. Multi-stage тут как раз помогает, если секрет остался в сборочном этапе и не был скопирован дальше.
Забывают про непривилегированного пользователя и про .dockerignore. Образ получается минимальным по зависимостям, но запускается от root, а в контекст сборки уезжает история репозитория вместе с локальным окружением.