Security guide · Romania

Why not leave node_modules and npm on a production server?

A node_modules folder built on the server brings in code nobody audited, install scripts that run with your application's rights and versions that differ from the ones you tested. Since the 2025 wave of compromised npm packages, the risk is no longer theoretical.

Updated:

Short answer

  1. 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.
  2. Build the application in CI from package-lock.json and ship only the build output and the production dependencies to the server.
  3. If you must install on the server, run npm ci --omit=dev --ignore-scripts and never plain npm install.
  4. 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
StepIn CIOn productionReason
Resolving versions (npm install)YesNoVersions are decided once, in the lockfile
preinstall / postinstall scriptsYes, controlledNoThe vector of the 2025 supply-chain attacks
Build (tsc, bundler)YesNoCompilers have no place in the runtime
Running the applicationNoYesOnly build output and production dependencies

Commands and configuration

Reproducible install, without devDependencies and without scripts
npm ci --omit=dev --ignore-scripts
.npmrc on the server and in CI
ignore-scripts=true
fund=false
Multi-stage Dockerfile with no npm in the runtime
FROM 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.

Request a security review

A production server starts getting attacked within minutes of receiving a public IP address, however small the project is. This guide ranks the controls by impact, from SSH access to incident response, with the Romanian rules that apply along the way. Every topic has its own page with a short answer, examples and ready-to-use configuration.