Short answer
- Keep node_modules and npm off production, because every installed package is third-party code that executes on your machine, with install scripts and development dependencies your application never needs.
- Build the application in CI from package-lock.json and ship only the build output and the production dependencies to the server.
- If you must install on the server, run npm ci --omit=dev --ignore-scripts and never plain npm install.
- In Docker, separate the build stage from the final image so the runtime contains no npm, no compilers and no devDependencies.
- Install command for production
- npm ci --omit=dev --ignore-scripts
- File that npm ci requires
- package-lock.json
- Attack vector seen in 2025
- preinstall scripts in compromised packages
What happens when node_modules stays on the server
Running npm install in production downloads hundreds of packages and executes their preinstall and postinstall scripts as the application user. If one of them is compromised, the attacker can read environment variables, .env files and the internal network. Without a strictly enforced lockfile you also get versions nobody tested in CI, so a bug seen on the server cannot be reproduced anywhere else. The npm cache may hold private-registry tokens, and devDependencies leave compilers and test tooling on a machine that should only run the application.
Why npm ci with --omit=dev and --ignore-scripts, not npm install
npm ci refuses to run without package-lock.json, wipes node_modules and installs exactly the locked versions, so you get the same dependency tree as in CI. --omit=dev leaves out devDependencies. --ignore-scripts switches off preinstall and postinstall, the vector used by the Shai-Hulud campaign in November 2025. None of this removes supply-chain risk entirely: chalk and debug were production dependencies when they were compromised. The real control is not to install on the server at all, but to build once in CI, where you can scan the packages.
What Romanian law says
OUG 155/2024, which transposes NIS2 and was approved by Law 124/2025, expects entities registered with the DNSC to manage supply-chain security, including third-party software components. A reproducible build with a lockfile and no ad-hoc installs on production is the simplest evidence that you know which code you run. If the application processes personal data, Article 32 GDPR calls for appropriate technical measures, and ANSPDCP looks at exactly this control after an incident.
Where each step runs in a Node.js delivery chain| Step | In CI | On production | Reason |
|---|
| Resolving versions (npm install) | Yes | No | Versions are decided once, in the lockfile |
|---|
| preinstall / postinstall scripts | Yes, controlled | No | The vector of the 2025 supply-chain attacks |
|---|
| Build (tsc, bundler) | Yes | No | Compilers have no place in the runtime |
|---|
| Running the application | No | Yes | Only build output and production dependencies |
|---|
Commands and configuration
Reproducible install, without devDependencies and without scriptsnpm ci --omit=dev --ignore-scripts
.npmrc on the server and in CIignore-scripts=true
fund=false
Multi-stage Dockerfile with no npm in the runtimeFROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]
Practical examples
SaaS team in Iasi, Node.js API on a VPS
The team ran npm install on the server at every deploy. Once the build moved to GitHub Actions and only the dist folder plus production dependencies reached the server, a whole class of works-on-my-machine bugs disappeared.
Web agency in Timisoara, Next.js site
A compromised development package was caught in CI by the dependency scanner before the deploy. Because the production server had neither npm nor devDependencies, the incident stopped at the pipeline and never touched client data.
Common mistakes
- Assuming npm install --production on the server is just as safe, when without a lockfile it is not reproducible and install scripts still run.
- Believing --omit=dev removes the supply-chain risk, although production dependencies can be compromised too.
- Not committing package-lock.json and letting every deploy resolve versions from scratch.
- Keeping npm and compilers in the final Docker image for quick debugging.
How 4hosting helps
- VPS plans include 1–8 hours of DevOps per month, dedicated servers 5–10 hours depending on the plan and GPU servers 10 hours; you request them from the client panel, in the “DevOps Services” section, for example to move your build into CI.
- CI/CD & Monitoring sets up GitHub Actions or GitLab CI with a reproducible build and dependency scanning.
- Docker & Kubernetes builds multi-stage images that carry no npm in the runtime.
Related questions
Can I use npm ci --ignore-scripts if I have native packages such as sharp?
Yes. Many native packages no longer compile at install time: sharp ships prebuilt binaries through optional dependencies since version 0.33, so --ignore-scripts does not affect it, although --omit=optional would break it. For a package that really needs its script, run npm ci --ignore-scripts, then npm rebuild for that package explicitly. The allowed list stays short.
What is the difference between npm ci and npm install?
npm ci requires a lockfile, deletes node_modules and installs exactly the pinned versions without touching package-lock.json. npm install resolves versions again and may rewrite the lockfile, so two runs can produce different trees. On production and in CI you always use npm ci.
Do I need this for a small site with only a few packages?
Yes, because the number of packages you declare says nothing about the full tree, which often reaches hundreds of transitive dependencies. A multi-stage Dockerfile or a simple build pipeline costs a few hours once.