БОЛЬ

CI pipeline: 47 коммитов в день, каждый запускает docker build. Сборка занимает 8 минут, из них 7 – npm ci. Вы поменяли одну строку в src/index.ts, но Docker заново устанавливает все 1200 зависимостей. Каждый. Раз.

За день это 47 * 7 = 329 минут впустую. За месяц – 110 часов ожидания. И всё потому, что COPY . . стоит перед RUN npm ci.

КАК УСТРОЕНО

Docker собирает образ слоями. Каждая инструкция FROM, COPY, RUN создаёт слой. Слои кешируются.

Правило кеша: если инструкция и всё, что было до неё, не менялись – Docker берёт слой из кеша. Но если слой инвалидирован – все последующие слои тоже пересобираются.

Плохой порядок:

COPY . .          # <-- любое изменение файла инвалидирует кеш
RUN npm ci        # <-- пересобирается каждый раз

Хороший порядок:

COPY package.json package-lock.json ./   # <-- меняется редко
RUN npm ci                                # <-- кешируется!
COPY . .                                  # <-- только исходники

Принцип: от редко меняющегося к часто меняющемуся. Зависимости меняются раз в неделю, код – 47 раз в день. Разнесите их по разным слоям.

ПРАКТИКА

Dockerfile для Node.js-приложения тормозит CI. Расставьте инструкции так, чтобы npm ci кешировался при изменении исходников.

Расставьте Dockerfile для максимального кеша

  • COPY . .
  • RUN npm ci
  • COPY package.json package-lock.json ./
  • FROM node:22-alpine
  • WORKDIR /app
  • RUN npm run build
  • CMD ["node", "dist/index.js"]
Сначала package.json и package-lock.json – они меняются редко. npm ci устанавливает зависимости и кешируется, пока lock-файл не изменился. COPY . . копирует исходники – они меняются часто, но инвалидируют только слои ниже (build, CMD), а не npm ci.

РАЗБОР

# 1. Базовый образ -- меняется раз в квартал
FROM node:22-alpine

# 2. Рабочая директория -- никогда не меняется
WORKDIR /app

# 3. Lock-файлы -- меняются раз в неделю
COPY package.json package-lock.json ./

# 4. Установка зависимостей -- кешируется пока lock-файл тот же
RUN npm ci

# 5. Исходники -- меняются каждый коммит
COPY . .

# 6. Сборка -- пересобирается при изменении кода (это ок)
RUN npm run build

# 7. Запуск
CMD ["node", "dist/index.js"]

Результат: при изменении кода Docker использует кеш для шагов 1–4 (FROM, WORKDIR, COPY lock-файлов, npm ci) и пересобирает только шаги 5–7. Вместо 8 минут – 30 секунд.

Проверить кеширование можно в выводе docker build:

=> CACHED [2/6] WORKDIR /app
=> CACHED [3/6] COPY package.json package-lock.json ./
=> CACHED [4/6] RUN npm ci
=> [5/6] COPY . .

CACHED – значит слой взят из кеша.

ВОПРОС НА СОБЕСЕ

Вопрос: Как работает кеширование слоёв Docker и когда кеш инвалидируется?

Показать ответ

Docker кеширует каждый слой (результат инструкции). При повторной сборке проверяет:

  1. FROM: совпадает ли базовый образ (тег + digest)
  2. RUN: совпадает ли текст команды (посимвольно)
  3. COPY/ADD: совпадают ли checksum’ы копируемых файлов

Если на каком-то шаге кеш инвалидирован – все последующие шаги пересобираются, даже если сами не менялись. Это называется “cache bust”.

Типичные причины инвалидации:

  • COPY . . перед RUN npm install – любой файл в контексте инвалидирует кеш
  • RUN apt-get update && apt-get install -y curl без --no-cache – текст не менялся, кеш работает, но пакеты могут устареть
  • ARG VERSION=1.0 – изменение ARG инвалидирует все слои после его использования

Лучшие практики: сортировать инструкции от редко меняющихся к часто меняющимся. Lock-файлы отдельно от исходников. Объединять RUN-команды с && чтобы уменьшить количество слоёв.