useCallback в React: когда реально нужен, а когда вредит
Типичный 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.