RE:NODE

Приложения12 мин чтения

npm ci и npm install: что из них нужно в деплое

Чем npm ci отличается от npm install, почему lockfile обязан быть в репозитории и как оставить devDependencies для сборки и убрать их после неё.

0 прочтений

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

Что на самом деле делает каждая команда#

npm installnpm ci
Нужен lockfileНетДа, иначе завершается с ошибкой
Записывает lockfileДа, когда нужноНикогда
Существующий node_modulesИспользует и дополняетСначала удаляется
Диапазоны версий пакетовЗаново разрешает ^ и ~Игнорирует, ставит дерево как есть
Рассинхронизированный package.jsonМолча исправляетПадает с EUSAGE
Установить один пакетnpm install lodashНе поддерживается
Обычная скоростьМедленнееЧасто примерно вдвое быстрее

npm install читает package.json, видит диапазоны вроде "express": "^4.18.2" и вычисляет дерево, которое им удовлетворяет. Если lockfile существует и его версии по-прежнему удовлетворяют этим диапазонам, он использует их. Если нет - потому что кто-то правил package.json вручную или зависимость добавили в другой ветке, - он разрешает всё заново и переписывает lockfile. На ноутбуке это удобно. На сервере это значит, что ваш деплой тихо поставил то, что вы ни разу не запускали.

npm ci разрешением не занимается вообще. В lockfile уже лежит полное дерево: каждый пакет, его точная версия, разрешённый URL и хеш целостности. npm ci разворачивает это дерево и останавливается. Он детерминирован в сильном смысле: один и тот же lockfile даёт один и тот же node_modules на любой машине, в любой день, что бы ни было опубликовано с тех пор.

Побочный эффект, о котором стоит знать: ошибки ERESOLVE о peer-зависимостях - явление npm install. Они возникают при разрешении, а npm ci не разрешает. Если вам пришлось запускать локально npm install --legacy-peer-deps, получившееся дерево уже зашито в lockfile, и серверу этот флаг не нужен.

Почему lockfile решает всё#

package-lock.json - это не артефакт сборки. Это исходный код, и он идёт в git. Самая частая причина «у меня в разработке работало» - .gitignore, в котором есть package-lock.json, обычно скопированный из шаблона библиотеки, где его исключение можно оправдать. Для приложения нельзя никогда.

Внутри каждая запись выглядит примерно так:

json
"node_modules/express": {  "version": "4.18.2",  "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",  "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",  "dependencies": { "accepts": "~1.3.8" }}

Хеш integrity делает воспроизводимую установку свойством безопасности, а не только удобством: если tarball по этому URL когда-нибудь изменится, установка упадёт, а не запустит другой код под тем же номером версии.

Поле заголовка lockfileVersion показывает, какой npm его записал:

ВерсияКем записанаПримечания
1npm 6Нет полных метаданных дерева; новый npm обновляет его
2npm 7 и 8Содержит оба формата, так что npm 6 всё ещё может его читать
3npm 9 и 10 для новых проектовМеньше, требует npm 7 или новее

Разные версии npm в команде - вот как получается lockfile-diff на 4000 строк в pull request, менявшем одну зависимость. Закрепите вместо этого среду выполнения: файл .nvmrc и блок engines, говорящий, что вы поддерживаете.

package.json
{  "engines": { "node": ">=20.0.0", "npm": ">=10.0.0" }}

По умолчанию engines носит рекомендательный характер. Добавьте engine-strict=true в .npmrc, чтобы несовпадение стало ошибкой, - это правильное решение для проекта, где версия Node на сервере фиксирована, а на вашем ноутбуке нет.

devDependencies, сборки и ловушка посередине#

--omit=dev (современное написание устаревшего --production) пропускает всё из devDependencies. На сервере это то, что нужно: ни компилятора TypeScript, ни тест-раннера, ни бандлера, на сотни мегабайт меньше на диске.

За исключением того, что typescript, vite, webpack, esbuild, tailwindcss и prisma - это все dev-зависимости, и все они нужны, чтобы получить то, что вы собираетесь запускать. Поэтому вот это падает:

bash
$ npm ci --omit=dev$ npm run buildsh: tsc: not found

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

Сборка на сервере. Установите всё, соберите, затем очистите:

bash
$ npm ci --no-audit --no-fund$ npm run build$ npm prune --omit=dev

npm prune --omit=dev убирает dev-пакеты из существующего node_modules, не трогая ничего другого. Этот шаг люди пропускают, и он обычно стоит нескольких сотен мегабайт.

Сборка в другом месте. Если готовый результат производит задача в CI или ваш ноутбук и отправляет его дальше, серверу всегда нужны только рантайм-пакеты:

bash
$ npm ci --omit=dev --no-audit --no-fund$ node dist/server.js

Не рассчитывайте, что NODE_ENV=production сам отбросит dev-зависимости. Это поведение отличалось между версиями npm, а деплой, зависящий от побочного эффекта переменной окружения, ломается при обновлении. Передайте флаг.

Ещё одно следствие отказа от dev-зависимостей: скрипт жизненного цикла prepare не запускается. Проекты, которые собирают себя в prepare, - это обычное дело с Husky и с пакетами, установленными прямо из URL git, - тихо не произведут ничего. Если ваша установка зависит от prepare, вам нужна полная установка.

Последовательность деплоя, которая работает#

По порядку деплой Node на небольшом сервере выглядит так:

  1. Получите код, включая package-lock.json. Если lockfile отсутствует, остановитесь и исправьте это, прежде чем делать что-либо другое.
  2. `npm ci` с dev-зависимостями, если сборка происходит здесь.
  3. Сборка - npm run build, или tsc, или что там производит dist.
  4. Выполните миграции, если релизу они нужны, и сделайте их обратно совместимыми, чтобы старый процесс мог обслуживать запросы, пока запускается новый. Подробно об этом - в статье миграции без простоя.
  5. `npm prune --omit=dev`, чтобы сбросить инструменты сборки.
  6. Перезапустите процесс и следите за логом, пока он не сообщит, что слушает порт.

В RE:NODE первый шаг - это функция Git deploy в тарифах для приложений: GitHub через GitHub App с короткоживущими токенами, так что приватные репозитории работают без того, чтобы вы где-либо вставляли персональный токен доступа. Ею управляют два переключателя - pull при каждом запуске и deploy при push, который перезапускает сервер, только если он уже был запущен, - и каждый деплой оставляет запись. Настройка пошагово описана в статье деплой приложения Node из GitHub.

Сбой, от которого эта последовательность предохраняет, - это цикл падений. Если установка падает, процесс завершается, что-то перезапускает его, установка падает снова. В RE:NODE за этим следит наблюдатель: три перезапуска за час выставляют предупреждение на странице сервера и автоматически открывают тикет, а шесть приостанавливают сервер. Это намеренный тормоз, а не наказание, но гораздо проще, если шаг 2 упадёт один раз, наглядно, в консоли, которую вы читаете. На что смотреть, описано в статье как читать консоль.

Если деплой может положить сайт, его должно быть можно откатить за минуту. Сохраняйте предыдущую сборку и делайте backup перед обновлением зависимости, затрагивающей что-либо нативное, - схемы переключения, подходящие для одной машины, есть в статье деплой без простоя на небольшом сервере.

Время установки, кэш и диск на небольшом тарифе#

Удаление node_modules командой npm ci звучит дорого и по большей части таковым не является, потому что сами пакеты берутся из локального кэша ~/.npm/_cacache, а не из сети. Тёплый кэш превращает большинство установок в копирование файлов.

Флаги, которые действительно помогают на небольшом сервере:

  • --no-audit - пропускает проверку уязвимостей, которая запускается в конце установки. Ей нужно обращение по сети, и она печатает стену текста, которую никто не читает во время деплоя. Проводите аудит в CI.
  • --no-fund - убирает уведомление о финансировании. Косметика, но лог становится короче.
  • --prefer-offline - использует кэшированные данные, где может, и обращается к реестру только за недостающим. Полезно, когда реестр медленный, а не недоступен.
  • --ignore-scripts - отказывается запускать установочные скрипты зависимостей. Хорошо для безопасности цепочки поставок, но ломает любой пакет, компилирующий нативный код при установке, так что проверьте, прежде чем принимать.

С диском люди сталкиваются первым. Обычный API на Express - это 50-100 МБ node_modules; приложение Next.js с UI-библиотекой обычно занимает 500-800 МБ ещё до того, как вы что-либо собрали, а рядом лежат результат сборки и кэш npm. На тарифе с 5 ГБ это не погрешность. Две привычки удерживают это под контролем: очищайте после сборки и время от времени чистите кэш командой npm cache clean --force, - хотя учтите, что после этого следующая установка станет медленнее, так что делайте это, когда диска действительно мало, а не по расписанию.

Память - другая проблема. Установка дёшева; бандлинг - нет. tsc и next build на крупном проекте обычно хотят больше гигабайта, а на тарифе в 1 ГБ убивают сборку, а не приложение. В RE:NODE достижение лимита памяти останавливает контейнер и перезапускает его начисто вместо swap, так что симптом - сборка, исчезающая посреди выполнения без ошибки. Если это про вас, либо собирайте в другом месте и деплойте результат, либо перейдите на ступень выше на время сборки. Флаги heap и то, чего они не исправляют, разобраны в статье лимиты памяти Node.

Когда npm ci отказывается и что означает ошибка#

`npm ci` can only install packages when your `package.json` and `package-lock.json` are in sync. В lockfile нет чего-то, что просит package.json. Кто-то правил package.json напрямую, или слияние неудачно разрешило lockfile. Исправьте это на своей машине командой npm install, закоммитьте обновлённый lockfile, задеплойте снова. Не «исправляйте» это переключением сервера на npm install: так рассинхронизация прячется навсегда.

`The npm ci command can only install with an existing package-lock.json`. В репозитории нет lockfile. Сгенерируйте его командой npm install, закоммитьте.

Конфликты слияния в lockfile. Никогда не правьте вручную. Возьмите любую сторону, затем приведите в порядок:

bash
$ git checkout --theirs package-lock.json$ npm install$ git add package-lock.json

npm install с корректным package.json переписывает согласованный lockfile, а это единственный способ получить действительно допустимый.

`Cannot find module @rollup/rollup-linux-x64-gnu` или подобный пакет для конкретной платформы. Необязательные зависимости разрешаются для каждой платформы, и lockfile, сгенерированный на macOS или Windows, может не содержать бинарник для Linux, который нужен вашему серверу. Перегенерируйте lockfile на Linux - в контейнере, соответствующем серверу, или в CI - и закоммитьте. Удаление node_modules на сервере не поможет, потому что недостающая запись находится в lockfile.

`EBADENGINE`. Установленные Node или npm не удовлетворяют engines. Либо на сервере старая среда выполнения, либо пакет новее, чем вы думали. Проверьте node -v и npm -v, прежде чем считать пакет виноватым.

`EACCES` или `EPERM` при установке. Процесс не может писать туда, куда пытается. На хостинге на контейнерах это обычно означает что-то за пределами собственного каталога сервера, недоступное для записи по замыслу.

`ENOSPC`. Кончился диск. du -sh node_modules .npm покажет, какая из двух папок его съела.

Как сохранять lockfile честным#

Lockfile полезен, только если он правдив. Три привычки поддерживают это.

Машина одного человека - не эталон. Запускайте npm ci в CI на каждый pull request. Он падает на lockfile, который не закоммитили, и падает на lockfile, который больше не совпадает с package.json, - это две ошибки, которые доходят до продакшена.

Обновляйте намеренно. npm outdated показывает, что сдвинулось. npm update уважает ваши диапазоны и поднимает версии внутри них; смена мажорной версии - это правка package.json и затем npm install. Обновляйте по одной группе за раз, чтобы у сломанного деплоя был один подозреваемый.

Проверяйте, что вы на самом деле отгружаете. npm ls --omit=dev --depth=0 печатает рантайм-дерево. Это короткий список, и если читать его раз в квартал, находятся пакеты, о добавлении которых никто не помнит.

У других менеджеров пакетов аналог npm ci - это флаг, а не команда:

ИнструментВоспроизводимая установка
npmnpm ci
Yarn 1yarn install --frozen-lockfile
Yarn 2 и новееyarn install --immutable
pnpmpnpm install --frozen-lockfile (в CI - по умолчанию)
Bunbun install --frozen-lockfile

Каким бы вы ни пользовались, коммитьте один lockfile и только один. Репозиторий, в котором есть и package-lock.json, и yarn.lock, рано или поздно установит два разных дерева в зависимости от того, кто что запускал, и вы узнаете об этом в тот день, когда это важно.

Приватные реестры и пакеты со scope требуют учётных данных, которым место в переменной окружения, а не в закоммиченном .npmrc. npm раскрывает ${NPM_TOKEN} в .npmrc при чтении, так что файл можно коммитить, а значение - нет; о том, где это значение должно жить, см. переменные окружения и секреты. Если рядом с продакшеном стоит staging-сервер, дайте ему собственную копию каждого секрета; разделение описано в статье staging и продакшен в одном аккаунте.

FAQ#

Нужно ли коммитить package-lock.json?

Да, для любого приложения. Это запись того, что вы тестировали. Единственное оправданное исключение - библиотека, публикуемая в реестр, где потребители разрешают собственное дерево, а ваш lockfile влияет только на вашу собственную разработку.

Быстрее ли npm ci, чем npm install?

Обычно да, нередко примерно вдвое, потому что он полностью пропускает разрешение зависимостей. Разрыв больше всего при тёплом кэше и большом дереве. Но скорость - бонус: причина использовать его - детерминизм.

Можно ли запускать npm ci без devDependencies?

Да, через npm ci --omit=dev, но только если ничто в вашей сборке в них не нуждается. Если вы собираете на той же машине, установите всё, соберите, а затем выполните npm prune --omit=dev, чтобы убрать их.

Что, если lockfile нет вообще?

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

Почему мой деплой поставил не ту версию, что на ноутбуке?

Потому что на сервере запустили npm install с lockfile, который его не ограничивал, или вообще без lockfile. Диапазон с кареткой вроде ^4.18.2 принимает любой релиз 4.x, и один из них был опубликован между вашим тестом и деплоем.

Удаляет ли npm ci мои загруженные файлы?

Он удаляет node_modules и ничего больше. Всё, что вы храните вне этой папки, - загрузки, файл SQLite, сгенерированный конфиг - остаётся нетронутым. Пострадать можно только одним способом: хранить записываемые данные внутри node_modules, и стоит проверить, что вы этого не делаете.


Комментарии

Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.

0/2000