На каком этапе сборки Docker-образа следует устанавливать зависимости приложения?

«На каком этапе сборки Docker-образа следует устанавливать зависимости приложения?» — вопрос из категории DevOps, который задают на 26% собеседований Data Scientist / ML Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Зависимости следует устанавливать на раннем этапе сборки (build stage), но после копирования файла менеджера зависимостей (например, requirements.txt, package.json). Это критически важно для эффективного использования кэша слоев Docker.

Оптимальная последовательность в Dockerfile:

  1. Установить базовый образ.
  2. Скопировать файл со списком зависимостей.
  3. Установить зависимости.
  4. Скопировать остальной код приложения.

Пример для Python-приложения:

FROM python:3.11-slim as builder

# Устанавливаем системные зависимости, если нужны для компиляции Python-пакетов
RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 1. Копируем ТОЛЬКО файл зависимостей
COPY requirements.txt .

# 2. Устанавливаем зависимости. Этот слой будет закэширован.
RUN pip install --user --no-cache-dir -r requirements.txt

# Финальный, минимальный образ
FROM python:3.11-slim
WORKDIR /app

# Копируем установленные зависимости из стадии builder
COPY --from=builder /root/.local /root/.local

# 3. Копируем весь код приложения (этот слой меняется чаще всего)
COPY . .

# Добавляем локальные pip-пакеты в PATH
ENV PATH=/root/.local/bin:$PATH

CMD ["python", "main.py"]

Почему этот порядок важен?

  • Кэширование: Слой RUN pip install ... кэшируется Docker. Если requirements.txt не изменился, при следующей сборке Docker повторно использует кэшированный слой с уже установленными зависимостями, что сильно ускоряет процесс.
  • Если бы мы скопировали весь код (COPY . .) до установки зависимостей, то любое изменение в любом файле кода приводило бы к инвалидации кэша, и зависимости переустанавливались бы каждый раз.
  • Multi-stage build (использование as builder) позволяет оставить в итоговом образе только зависимости времени выполнения, без компиляторов и промежуточных артефактов, что уменьшает размер финального образа.