БОЛЬ
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 и когда кеш инвалидируется?