Dockerfile для Node.js: multi-stage сборка и маленький образ

Категория: docker · Язык: Dockerfile · Обновлено:

Многоступенчатая сборка (multi-stage) использует в одном Dockerfile несколько образов: в первом приложение собирается со всеми инструментами, во втором остаётся только результат. Итоговый образ Node.js не содержит исходников, dev-зависимостей и кэша сборки, поэтому меньше и безопаснее.

Структура проекта

Текст
my-node-app/
├── Dockerfile
├── .dockerignore
├── package.json
├── package-lock.json
└── src/server.js

Минимальный package.json со сборочным шагом:

Текст
{
  "name": "my-node-app",
  "version": "1.0.0",
  "scripts": {
    "build": "mkdir -p dist && cp src/server.js dist/server.js",
    "start": "node dist/server.js"
  }
}

Сервер src/server.js:

JavaScript
const http = require("node:http");

http
  .createServer((req, res) => {
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify({ status: "ok" }));
  })
  .listen(3000, "0.0.0.0");

В настоящем проекте на месте cp обычно стоит tsc, vite build или webpack.

Dockerfile

Dockerfile
# Этап 1: сборка
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Этап 2: запуск
FROM node:22-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

Что здесь происходит:

Строка

Смысл

AS build

Даёт имя этапу, чтобы ссылаться на него позже

COPY package*.json ./ до COPY . .

Зависимости кэшируются и не ставятся заново при правке кода

npm ci

Точная установка по package-lock.json, быстрее и воспроизводимее, чем npm install

npm ci --omit=dev

В итоговый образ попадают только рабочие зависимости

COPY --from=build

Забирает из первого этапа только готовую папку dist

USER node

Запуск без прав root, пользователь node уже есть в официальном образе

Файл .dockerignore

Текст
node_modules
dist
.git
.env
npm-debug.log
Dockerfile

Папку node_modules обязательно исключайте: иначе локальные модули, собранные под вашу систему, затрут установленные в образе.

Собрать и запустить

Bash
docker build -t my-node-app .
docker run -d --name my-node-app -p 3000:3000 my-node-app
curl http://localhost:3000

Собрать только один этап

Полезно для отладки сборки или запуска тестов:

Bash
docker build --target build -t my-node-app:build .

Сравнить размеры образов

Bash
docker images my-node-app

Образ из одной ступени, где остаются исходники и dev-зависимости, обычно заметно больше. Для ещё меньшего размера подойдёт node:22-alpine, но проверьте совместимость нативных модулей.

Частые вопросы

Зачем второй раз копировать package.json?

Во втором этапе устанавливаются только рабочие зависимости. Они не совпадают с набором первого этапа, поэтому npm ci запускается снова.

Как запускать тесты при сборке?

Добавьте RUN npm test в этап build до сборки. Если тесты упадут, образ не соберётся.

Почему приложение недоступно снаружи?

Сервер должен слушать 0.0.0.0, а не localhost: иначе порт контейнера не принимает соединения от хоста.