lenec ru

← все посты

useCallback в React: когда реально нужен, а когда вредит

17K

Типичный Docker-образ Node.js-приложения на базе node:18 весит больше гигабайта. В проде вам не нужны ни компилятор, ни dev-зависимости, ни исходники TypeScript. Multi-stage builds решают эту проблему: несколько этапов в одном Dockerfile, а в финальный образ попадает только необходимое для запуска.

Почему образы раздуваются

Основные причины:

  • Базовый node:18 (Debian) — ~900 MB с gcc, make, python3
  • node_modules с dev-зависимостями — линтеры, тесты, типы
  • Исходники TypeScript, конфиги сборщиков
  • Кэш npm/yarn и временные файлы

Каждый лишний мегабайт — медленный деплой и увеличенная поверхность атаки.

Принцип multi-stage

Несколько инструкций FROM в одном Dockerfile. Каждый FROM — новый этап. Из предыдущих этапов копируете файлы через COPY --from=stage, остальное отбрасывается:

FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/main.js"]

Полный пример: Express + TypeScript

Три этапа — сборка, prod-зависимости, финальный образ:

# Stage 1: build
FROM node:18-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build

# Stage 2: prod deps
FROM node:18-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Stage 3: final
FROM node:18-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=build /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY package.json ./
USER app
EXPOSE 3000
CMD ["node", "dist/main.js"]

Финальный образ содержит только Alpine, собранный JS и production-зависимости. Непривилегированный пользователь — бонус к безопасности.

Alpine vs Distroless

Два варианта финального базового образа:

  • node:18-alpine (~50 MB) — есть shell, можно дебажить
  • gcr.io/distroless/nodejs18-debian12 (~40 MB) — без shell, максимальная безопасность
FROM gcr.io/distroless/nodejs18-debian12
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
CMD ["dist/main.js"]

Сравнение размеров

$ docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY   TAG            SIZE
myapp        naive          1.24 GB
myapp        alpine-multi   147 MB
myapp        distroless     128 MB

С 1.24 GB до 128 MB — уменьшение почти в 10 раз.

Подводные камни

  • Native-модули — если зависимость компилирует C-аддоны (sharp, bcrypt), финальный образ должен содержать нужные .so-библиотеки
  • .dockerignore — без него COPY . . затянет node_modules с хоста. Добавьте node_modules, dist, .git, .env*
  • Кэш слоёв — копируйте package*.json отдельно перед npm ci, чтобы Docker кэшировал установку при неизменном lock-файле

Итог

Multi-stage — стандартная практика для Node.js в проде. Собрать → отсеять dev-зависимости → минимальный базовый образ. Результат — в 8-10 раз легче, быстрее деплоится, меньше поверхность атаки. Если у вас один FROM node:18 для прода — время переписать Dockerfile.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.