Дело о пропавших гигабайтах: расследуем Docker-образ Node.js
Пятница, 19:47. Джун-дев Макс получает тревогу: деплой длится 15 минут — образы весят 1,4 ГБ. Надо срочно найти, где прячутся лишние мегабайты, и похудеть контейнер.
Шаг 0. Собираем улики
- Устанавливаем dive — консольный профайлер контейнеров.
brew install dive dive my-app:latest
Tool показывает, какие команды добавили «мусор» и каков вес каждого слоя. - Фиксируем исходную массу тела: 1 400 МБ.
Шаг 1. .dockerignore — первый кордон полиции
Новички часто пишут COPY . .
и тянут в образ тесты, git-историю и кэш IDE. В файл .dockerignore
добавляем:
node_modules
.git
*.log
coverage
Минус десятки мегабайт за один ход.
Шаг 2. Подозреваемый № 1 — node_modules
В проде dev-зависимости (ESLint, Jest) не нужны.
Меняем установку:
RUN npm ci --omit=dev
Экономия ≈ 300 МБ на среднем проекте.
Шаг 3. Каждый RUN
— новый слой
Несколько RUN
= несколько слоёв. Склеиваем команды:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
Шаг 4. Перехват: Multi-Stage Build
Как это работает
Dockerfile
содержит два этапа:
- Stage 1 — build: собираем приложение.
- Stage 2 — run: берём только артефакты и чистый runtime.
Минимальный рецепт для Node.js
# ---------- Stage 1: build ----------
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build # кладёт JS в /app/dist
# ---------- Stage 2: run ----------
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist .
CMD ["server.js"]
Размер образа падает до ≈ 240 МБ.
Шаг 5. Финальный удар: Distroless
Distroless-базы содержат только runtime-библиотеки, без bash
и apt
.
Это ещё −30 % веса и минимум CVE. Для отладки используйте теги *-debug
или подключайте временный busybox.
Итоги расследования
Этап | Вес образа |
---|---|
До оптимизации | 1,4 ГБ |
.dockerignore + npm ci --omit=dev | 650 МБ |
Multi-Stage Build | 240 МБ |
Distroless | ≈ 120 МБ |
Прод катится за секунды, счёт за трафик падает, а Макс закрывает ноутбук с улыбкой: преступление лишних гигабайтов раскрыто.
Частые вопросы
Можно ли обойтись node:alpine
вместо Distroless?
Да, получится ≈ 180 МБ и проще дебажить (apk add bash
).
Как чистить кэш сборок?docker builder prune -af
удалит dangling-кэш и освободит гигабайты на ноутбуке.
Всегда ли убираем dev-зависимости?
Если у вас SSR или миграции Prisma в рантайме, dev-зависимости могут потребоваться. Тестируйте контейнер!