Dockerfile для Node.js: multi-stage сборка и маленький образ
Многоступенчатая сборка (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:
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
# Этап 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"]Что здесь происходит:
Строка | Смысл |
|---|---|
| Даёт имя этапу, чтобы ссылаться на него позже |
| Зависимости кэшируются и не ставятся заново при правке кода |
| Точная установка по |
| В итоговый образ попадают только рабочие зависимости |
| Забирает из первого этапа только готовую папку |
| Запуск без прав root, пользователь |
Файл .dockerignore
node_modules
dist
.git
.env
npm-debug.log
DockerfileПапку node_modules обязательно исключайте: иначе локальные модули, собранные под вашу систему, затрут установленные в образе.
Собрать и запустить
docker build -t my-node-app .
docker run -d --name my-node-app -p 3000:3000 my-node-app
curl http://localhost:3000Собрать только один этап
Полезно для отладки сборки или запуска тестов:
docker build --target build -t my-node-app:build .Сравнить размеры образов
docker images my-node-appОбраз из одной ступени, где остаются исходники и dev-зависимости, обычно заметно больше. Для ещё меньшего размера подойдёт node:22-alpine, но проверьте совместимость нативных модулей.
Частые вопросы
Зачем второй раз копировать package.json?
Во втором этапе устанавливаются только рабочие зависимости. Они не совпадают с набором первого этапа, поэтому npm ci запускается снова.
Как запускать тесты при сборке?
Добавьте RUN npm test в этап build до сборки. Если тесты упадут, образ не соберётся.
Почему приложение недоступно снаружи?
Сервер должен слушать 0.0.0.0, а не localhost: иначе порт контейнера не принимает соединения от хоста.