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

Ответ

Зависимости следует устанавливать на раннем этапе сборки (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) позволяет оставить в итоговом образе только зависимости времени выполнения, без компиляторов и промежуточных артефактов, что уменьшает размер финального образа.

Ответ 18+ 🔞

А, ну ты держись, сейчас я тебе такую простыню накатаю про зависимости в Docker, что ты офигеешь. Это ж основа основ, а народ постоянно наступает на одни и те же грабли, ёпта.

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

Идеальный расклад в Dockerfile должен быть такой, чтобы даже кот соседский понял:

  1. Берём голый образ, на котором будем всё строить.
  2. Тащим в контейнер ОДИН файл — тот самый список зависимостей (requirements.txt, package.json — ну ты понял).
  3. Устанавливаем по этому списку всё, что нужно. Вот этот шаг и закэшируется на ура.
  4. И только ПОТОМ, когда всё установлено, закидываем туда весь наш остальной код, который меняется каждые пять минут.

Вот смотри, как для питонячего приложения выглядит нормальный Dockerfile, а не пиздопроебина какая-то:

FROM python:3.11-slim as builder

# Иногда для сборки каких-нибудь нативных библиотек нужен компилятор. Ставим его тут.
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 . .

# Чтобы система видела наши локально установленные питоньи пакеты
ENV PATH=/root/.local/bin:$PATH

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

А теперь, блядь, слушай сюда, почему это так работает, а иначе — пиздец:

  • Волшебный кэш: Этот шаг RUN pip install ... — он как отпечаток пальца. Если твой requirements.txt не менялся, Docker такой: «О, да я это уже делал!» — и берёт готовый слой из кэша. Сборка из трёх минут превращается в три секунды. Удивление пиздец, да?
  • А если бы ты скопировал весь код (COPY . .) в самом начале, то любое изменение в любом файлике — хоть в README запятую поправил — приводило бы к полной пересборке ВСЕГО, включая установку зависимостей. Это же терпения ноль ебать, каждый раз ждать.
  • Multi-stage build (это когда as builder пишем) — это вообще гениальная штука. Мы в первой стадии наворотили всего, что нужно для сборки (типа того компилятора gcc), а в финальный образ забрали только результат — сами библиотеки. Весь строительный мусор остался за бортом. Итоговый образ легче, чище, безопаснее. Красота, я тебе в рот чих-пых!