You’ve built your Docker image, and everything works fine locally. But when you deploy to production, things break. Or worse — they seem fine, until a dependency silently changes and your app crashes at 3 AM.
The culprit? Often, it's running npm install inside your production container.
Let’s break down why this is risky and what to do instead.
- Non-Deterministic Builds npm install fetches the latest compatible versions based on your ^ or ~ ranges. Today, that might mean lodash@4.17.20. Tomorrow, it could be 4.17.21 — with a subtle breaking change or a vulnerability fix that actually breaks your logic.
Your container image becomes a time bomb. Rebuilding the same image a week later could produce a different result.
Fix: Always use npm ci instead of npm install in production. It installs exactly what’s in your package-lock.json.
- Build Tools & Unnecessary Dependencies npm install often pulls in devDependencies by default (unless you pass --omit=dev). That means:
TypeScript compiler
Testing frameworks
Linters
Build tools like node-gyp
These add size, attack surface, and potential runtime issues — none of which belong in production.
Fix: Use npm ci --omit=dev or set NODE_ENV=production before install.
- Slower, Larger, Less Secure Images Running npm install inside your final container means:
No node_modules layer caching (unless carefully optimized)
Larger image size (gigabytes vs. megabytes)
Extra time during deployment
More importantly, every npm install re-runs scripts from packages. Malicious or compromised packages can execute arbitrary code during install — in your production environment.
Fix: Build node_modules in a separate build stage and copy only what’s needed.
- The Better Pattern: Multi-Stage Builds Here’s the safe, fast, reproducible way:
dockerfile
Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["node", "index.js"]
No network calls during production container start. No surprises. Small image.
- Exceptions to the Rule Sometimes you do need to run npm install in production:
You’re installing private packages that require runtime authentication
You’re deploying to an environment without build capabilities (e.g., some PaaS)
Even then, prefer npm ci and avoid running it repeatedly.
Summary
Practice Deterministic Fast Secure Production Ready
npm install (default) ❌ ❌ ❌ ❌
npm ci --omit=dev ✅ ✅ ⚠️ (still runs scripts) ⚠️
Pre-built + copy ✅ ✅✅ ✅ ✅
Don’t let your production container phone home for dependencies at runtime. Build once, copy carefully, and deploy with confidence.
Want to catch these issues before they reach production? Add npm ci to your CI pipeline and test against a locked package-lock.json.
Top comments (0)