Dockerfile для Node.js в 2026: сборка и оптимизация образа | AdminWiki

Dockerfile для Node.js в 2026: сборка и оптимизация образа

27 сентября 2026 12 мин. чтения

Dockerfile для Node.js в 2026 году строится по одной схеме: стадия builder собирает артефакт со всеми зависимостями, стадия runtime получает только прод-пакеты и готовый код. Версия базового образа фиксируется явно, установка идёт через npm ci, контейнер работает от пользователя node. Образ Express-сервиса при таком подходе обычно заметно меньше, чем сборка в одной стадии с devDependencies, но конкретные цифры зависят от проекта и версии базового образа.

Ниже шесть блоков, из которых состоит рабочий файл: базовый образ, зависимости, сборка, runtime, запуск, проверка. Пример рассчитан на Node.js 22 LTS: версия 22.22.0 «Jod» — это LTS-релиз, а LTS-статус означает долгосрочную поддержку, обычно гарантирующую исправление критических багов в течение 30 месяцев. Для продакшна следует использовать только Active LTS или Maintenance LTS. Сборка опирается на BuildKit: он является сборщиком по умолчанию для пользователей Docker Desktop и Docker Engine.

Нужен результат сразу: переходите к разделу с готовым Dockerfile, там полный файл, команды сборки и проверки. Остальные разделы объясняют, почему каждая строка выглядит именно так.

Что должно быть в Dockerfile для Node.js в 2026 году

Рабочий файл собирается из шести блоков, и каждый закрывает свою задачу.

  • Базовый образ. Фиксированная LTS-версия: FROM node:22-slim, в идеале с digest вместо плавающего тега.
  • Зависимости. COPY package.json package-lock.json до установки через npm ci с кэш-маунтом BuildKit.
  • Сборка. Отдельная стадия builder: компиляция TypeScript, сборка фронтенда и любые шаги, которым нужны devDependencies.
  • Runtime. Чистая стадия с прод-зависимостями: npm ci --omit=dev плюс артефакт, скопированный через COPY --from=builder.
  • Запуск. USER node, ENV NODE_ENV=production, EXPOSE, HEALTHCHECK и CMD в exec-форме.
  • Проверка. docker images, docker history, docker run с пробросом порта и curl по эндпоинту здоровья.

Какие практики устарели к 2026 году

Часть приёмов из старых гайдов продолжает кочевать по репозиториям и создаёт проблемы: раздутый образ, невоспроизводимую сборку, лишние права у процесса.

ПрактикаЧем плохаЧем заменить
npm install в сборкеможет переписать lock-файл и подтянуть версии, которых вы не проверялиnpm ci
Тег latest или только мажорная версияследующая сборка приезжает на другом базовом слое без вашего участияnode:22-slim с digest
Одна стадия с devDependenciesкомпиляторы, тестовые фреймворки и лишние сотни мегабайт в продеmulti-stage build
Запуск от rootкомпрометация приложения даёт root в контейнереUSER node
COPY . . до установки зависимостейправка одного файла переустанавливает все пакетысначала манифесты, потом код
Секреты через ENV и ARGтокен остаётся в истории слоёв и виден в docker historyBuildKit secrets
node_modules, скопированный с хостабинарные модули собраны под другую ОС и архитектуруустановка внутри образа

BuildKit — это бэкенд сборщика, используемый Docker: он улучшает производительность сборок по сравнению с legacy-сборщиком из более ранних версий Docker и поддерживает более сложные сценарии (BuildKit | Docker Docs). Кэш npm подключается без дополнительных переменных окружения: RUN --mount=type=cache,target=/root/.npm npm ci. Учтите, что BuildKit полностью поддерживает сборку только Linux-контейнеров; для Windows-контейнеров используется legacy-сборщик, а поддержка WCOW в BuildKit остаётся экспериментальной начиная с версии 0.13.

Выбор базового образа: alpine, slim или distroless

Варианты отличаются размером, реализацией libc и набором утилит внутри.

ОбразРазмерlibcКогда брать
node:22-alpineAlpine Linux значительно меньше большинства базовых образов дистрибутивов (~5 МБ), итоговый образ получается тоньшеmuslсервисы без нативных модулей, когда минимальный размер — главный приоритет
node:22-slimсущественно меньше полного образа; в одном из разборов переход на node:bookworm-slim дал 231 МБ вместо почти гигабайтаglibcрабочий выбор по умолчанию для продакшна
node:22-bookwormполный образ дистрибутива, заметно тяжелее slimglibcотладка, образы для CI с компиляторами и утилитами
node:22-distrolessминимальный набор содержимогоglibcминимальная поверхность атаки, если готовы отлаживать иначе

Точные размеры тегов меняются от релиза к релизу, поэтому сверяйте их в реестре и командой docker images в своём проекте. Сравнение размеров и состава базовых образов Node.js разбирается в материале Choosing the best Node.js Docker image, а официальные варианты тегов описаны на странице образа node в Docker Hub.

Разница glibc и musl проявляется на нативных модулях. Alpine использует musl libc, поэтому ПО часто сталкивается с проблемами в зависимости от глубины требований и предположений о libc. При этом многие популярные пакеты уже поставляют готовые бинарники под musl: например, sharp предоставляет скомпилированные бинарники sharp и libvips для Linux x64 и ARM64 с musl >= 1.2.5, и при установке пакетные менеджеры автоматически выбирают подходящие бинарники под текущую ОС и архитектуру (Installation | sharp). Если готовых musl-бинарников для нужной платформы нет, sharp придётся собирать из исходников — это медленнее, но нормально. Для работы sharp на Alpine могут потребоваться дополнительные системные пакеты, в частности libvips и libc6-compat, либо настройка на системный libvips вместо загрузки несовместимых предсобранных бинарников. Для bcrypt и node-canvas в подготовленных материалах подтверждающих данных о поведении на musl нет, поэтому проверяйте их отдельно на своей платформе.

Рекомендация простая: для большинства продакшн-сервисов берём node:22-slim с фиксированным digest. Если нативных модулей нет и размер критичен, подойдёт alpine. Distroless выбирают, когда в компании есть отдельный процесс отладки и вы готовы жить без shell внутри контейнера.

Как зафиксировать версию образа через digest

Digest привязан к конкретному содержимому манифеста, поэтому перезапись тега в реестре не подменит базовый слой в вашей сборке.

# получаем digest
docker pull node:22-slim
docker inspect --format='{{index .RepoDigests 0}}' node:22-slim

# в Dockerfile
FROM node:22-slim@sha256:1111111111111111111111111111111111111111111111111111111111111111 AS builder

Компромисс очевиден: при выходе патча безопасности digest обновляете вручную и пересобираете образ. Автоматизировать это помогают Renovate и Dependabot, которые создают pull request на смену digest. Механику фиксации версий, lock-файлов и проверки воспроизводимости разбираем в отдельном материале про сборку воспроизводимых Docker-образов.

Размеры образов и номера версий меняются от релиза к релизу. Сверяйте теги в реестре и фактический размер командой docker images в своём проекте, а не по цифрам из чужой статьи.

Установка зависимостей через npm ci и работа с node_modules

npm ci ставит строго по package-lock.json, перед установкой удаляет существующий node_modules и падает с ошибкой, если lock-файл разошёлся с package.json. npm install в контейнере такого поведения не даёт: он может обновить lock-файл и притянуть версии, которых в вашем окружении не было. Для образа это означает потерю воспроизводимости между сборками.

Порядок инструкций в builder:

WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .

Манифесты меняются редко, исходники - постоянно. Пока package.json и lock-файл не тронуты, слой с npm ci берётся из кэша, и правка одного контроллера в src не переустанавливает зависимости заново. Как устроена инвалидация слоёв и как её проверить на своей сборке, показываем в статье про кэширование слоёв в Dockerfile.

В builder ставим всё: npm ci. В runtime только прод-пакеты: npm ci --omit=dev. Флаг --omit=dev работает в npm 7 и новее, в старых версиях использовали --production. Проверить состав можно командой npm ls --omit=dev --depth=0 уже внутри контейнера.

Почему нельзя копировать node_modules с хоста

Каталог node_modules содержит бинарные модули, собранные под ОС и архитектуру хоста. Файлы .node, скомпилированные на macOS или Windows под arm64, в Linux-контейнере не загрузятся: вы получите ошибку вида invalid ELF header или cannot open shared object file. Рабочий путь один: установка внутри образа на той же платформе, где он будет запущен.

В .dockerignore обязательно попадают node_modules, .git, .env, dist и coverage. Так контекст сборки меньше, docker build быстрее, а .env с токенами не уезжает внутрь образа.

Multi-stage сборка: отделяем dev-зависимости от продакшн-образа

Схема из двух стадий: builder с полным набором пакетов и шагами сборки, runtime с прод-зависимостями и артефактом. Артефакт переносится одной инструкцией COPY --from=builder /app/dist ./dist.

Ключевой момент: node_modules в runtime ставится заново через npm ci --omit=dev, а не копируется из builder. Если скопировать каталог целиком, devDependencies приедут в прод и заметно увеличат образ. Повторная установка занимает секунды благодаря кэш-маунту и тому, что прод-пакетов обычно в разы меньше.

Ориентир по размеру: в разборе выбора базового образа переход с полного образа на node:bookworm-slim уменьшил размер с почти гигабайта до 231 МБ (Choosing the best Node.js Docker image). Итоговый размер вашего прод-образа зависит от числа зависимостей и базового тега, поэтому измеряйте его командой docker images, а не ориентируйтесь на цифры из статьи.

Как адаптировать multi-stage под TypeScript

Компиляция tsc идёт в builder, в runtime копируется только результат.

FROM node:22-slim AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

В runtime не нужны ни tsconfig.json, ни каталог src, ни typescript в зависимостях. Исключение: source maps для отладки прода. Если они нужны, копируйте файлы .map отдельным шагом и держите их вне публичной статики. Один из готовых шаблонов с graceful shutdown и non-root пользователем лежит в подборке Dockerfile для Python, Node.js и Go.

Как адаптировать multi-stage под Next.js

В next.config.js включаем output: 'standalone'. Next.js при сборке автоматически трассирует зависимости каждой страницы, чтобы определить все файлы, нужные для продакшн-деплоя, и создаёт папку .next/standalone, которую можно развернуть без установки node_modules (next.config.js Options: output | Next.js). Такой вывод включает только необходимые файлы и зависимости, а один Node.js-сервер может обслуживать приложение, поддерживая SSR, API routes и ISR (Containerize a Next.js application | Docker Docs).

FROM node:22-slim AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build

FROM node:22-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=builder /app/public ./public
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]

Запуск идёт через node server.js, а не next start: standalone-бандл не использует CLI и не тянет за собой dev-часть фреймворка. Если в проекте есть SSR или ISR, контейнеру нужен полноценный Node.js runtime, статикой отделаться не получится.

Для pnpm и yarn меняется только команда установки и путь к кэшу: pnpm install --frozen-lockfile с кэшем в /root/.local/share/pnpm/store, yarn install --immutable. В монорепозитории копируйте lock-файл и package.json всех воркспейсов, иначе установка упадёт на отсутствующем манифесте.

Оптимизация размера и скорости сборки

Приёмы, которые дают эффект без переписывания приложения:

  • .dockerignore с node_modules, .git, .env, dist, coverage, *.log, .vscode, .idea.
  • Порядок COPY: сначала манифесты, затем код. Так кэш слоя с зависимостями живёт долго.
  • Кэш-маунт BuildKit для npm: RUN --mount=type=cache,target=/root/.npm npm ci.
  • Объединение RUN-команд и очистка apt-кэша: rm -rf /var/lib/apt/lists/* в той же инструкции, где была установка.
  • --omit=dev в runtime и отдельная установка вместо копирования каталога из builder.
  • slim или alpine вместо полного bookworm-образа.

Разложить слои по весу помогает docker history, а нагляднее показывает структуру утилита dive. Фактический размер проверяйте командой docker images: он зависит от базового тега и числа зависимостей.

Что обязательно должно быть в .dockerignore

node_modules
.git
.gitignore
.env
.env.*
dist
build
coverage
*.log
.DS_Store
.vscode
.idea
Dockerfile
docker-compose*.yml
README.md

Файл решает две задачи. Первая: контекст сборки не включает сотни мегабайт node_modules и историю git, поэтому docker build стартует быстрее. Вторая: .env и локальные конфиги не попадают ни в контекст, ни в финальный образ. Порядок работы с COPY и ADD внутри контекста разбираем в отдельной статье про разницу COPY и ADD.

Безопасность контейнера с Node.js

Базовая гигиена закрывает большинство типовых рисков:

  • Non-root пользователь. Образ node:22-slim уже содержит пользователя node с UID 1000. Добавьте USER node после копирования файлов и выставьте права через --chown при COPY.
  • NODE_ENV=production. Отключает лишние проверки и часть dev-поведения библиотек.
  • Отключение lifecycle-скриптов. npm ci --ignore-scripts убирает выполнение postinstall из пакетов, которые вы не контролируете. Это ломает модули, собирающиеся из исходников, поэтому применяйте осознанно.
  • Read-only root filesystem. Запуск с --read-only и запись только в смонтированный tmpfs закрывает возможность подмены файлов приложения.
  • Ограничение привилегий. --cap-drop=ALL и --security-opt=no-new-privileges снимают права, которые Node.js-процессу не нужны.

Чек-лист с командами для сканирования образов и удаления привилегий собран в материале про безопасность Docker-контейнеров.

Как передавать секреты в сборку правильно

BuildKit secrets монтируются только на время выполнения инструкции и не остаются в слоях образа.

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

# сборка
docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp:1.0 .

Токен приватного реестра в ARG или ENV попадает в docker history и виден любому, у кого есть доступ к образу. Тот же принцип для runtime: пароли и ключи передавайте переменными окружения при запуске или через внешний менеджер секретов, а .env внутрь образа не копируйте.

Готовый Dockerfile для Node.js: пример с командами сборки и проверки

Файл для Express-сервиса на TypeScript: сборка в builder, прод-зависимости и dist в runtime.

# ---------- builder ----------
FROM node:22-slim AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

# ---------- runtime ----------
FROM node:22-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
COPY --from=builder --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD node -e "fetch('http://127.0.0.1:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
CMD ["node", "dist/index.js"]

Сборка и запуск:

docker build -t myapp:1.0 .
docker images myapp:1.0
docker run --rm -p 3000:3000 myapp:1.0
curl -I http://localhost:3000/health

Ожидаемый результат: приложение слушает порт 3000, curl отвечает строкой HTTP/1.1 200 OK. Размер образа смотрите в выводе docker images и сравнивайте с размером односторонней сборки в своём проекте. Если у вас сборщик без BuildKit, уберите параметры --mount из инструкций RUN, иначе сборка завершится ошибкой разбора Dockerfile.

Как проверить размер и состав образа

docker images myapp:1.0
docker history myapp:1.0
docker run --rm myapp:1.0 sh -c 'ls -la /app && du -sh /app/node_modules'

docker history показывает вес каждого слоя: самый тяжёлый обычно соответствует установке зависимостей. Состав runtime проверяем списком файлов: в /app должны лежать dist, package.json, package-lock.json и node_modules без typescript, jest и eslint. Если размер неожиданно большой, ищите devDependencies или копирование всего контекста.

Последнюю команду нельзя выполнить на distroless-образе: там нет shell. Для таких сборок проверяйте состав через docker cp или сторонние сканеры, например Trivy или Grype.

Чек-лист перед деплоем

  • Версия базового образа зафиксирована, в идеале через digest.
  • Установка идёт через npm ci, а не npm install.
  • Сборка разбита на стадии builder и runtime.
  • В runtime стоит npm ci --omit=dev.
  • Процесс запускается от пользователя node.
  • Выставлен NODE_ENV=production.
  • .dockerignore исключает node_modules, .git, .env, dist, coverage.
  • Секреты передаются через BuildKit secrets, а не через ENV и ARG.
  • HEALTHCHECK настроен и отвечает 200 при старте.
  • Размер и состав образа проверены командами docker images, docker history и docker run.

Скопируйте Dockerfile, соберите образ и прогоните чек-лист: этого достаточно, чтобы получить воспроизводимую сборку и прод-образ без лишних зависимостей. Дальше остаётся встроить те же шаги в CI и обновлять digest базового образа при выходе патчей безопасности.

Размеры образов, номера версий и даты релизов меняются. Сверяйте теги и версии Node.js по официальным источникам проекта и проверяйте фактический размер в своём окружении: цифры в примерах зависят от проекта и версии базового образа.
Поделиться: