All articles

The Problem with Npm Install

Why npm ci Could Save Your Production Builds (and Your Sanity)

Bob⚡James
· 8 min read

Your build worked yesterday. Today it's broken.

Sound familiar? You're not alone. Thousands of developers face this exact scenario every day, and the culprit might surprise you: npm install itself.

While you're debugging "works on my machine" issues, your competitors are shipping reliable code with predictable builds.

Ready to never face another mysterious build failure again? 🤔



The Problem with npm install

When you run npm install, npm performs dependency resolution - it looks at your package.json and tries to figure out the "best" versions to install based on:

  • Semantic version ranges (e.g., ^1.2.3, ~2.1.0)
  • Current registry state
  • Peer dependency requirements
  • npm's internal resolution algorithms

This means npm install can produce different results at different times, even with the same package.json.

How npm ci Solves This

npm ci (Clean Install) takes a fundamentally different approach:

  • Uses package-lock.json as the single source of truth
  • Installs exact versions specified in the lockfile
  • Skips dependency resolution entirely
  • Always produces identical results

Key Differences

Aspect npm install npm ci
Source of Truth package.json + dependency resolution package-lock.json (exact versions)
Dependency Resolution ✅ Performs resolution ❌ Skips resolution
Lockfile Changes May update package-lock.json Never modifies lockfile
Speed Slower (resolution overhead) Faster (no resolution)
Reproducibility ❌ Can vary between runs ✅ Identical every time
node_modules Preserves existing files Deletes first (clean slate)
Failure Behavior Tries to resolve conflicts Fails fast if lockfile conflicts

Why This Matters for Production

1. Reproducible Builds

Developer's machine (works)

npm install # Gets working versions

CI/CD pipeline (may fail)

npm install # Gets different versions due to registry updates

With npm ci:

Developer's machine

npm install && git add package-lock.json && git commit

CI/CD pipeline

npm ci # Gets EXACT same versions as developer

2. Eliminates "Works on My Machine"

  • Different npm versions resolve dependencies differently
  • Registry updates can introduce breaking changes
  • Time-based dependency resolution can vary
  • npm ci bypasses all these variables

3. Security Benefits

  • Prevents supply chain attacks via dependency confusion
  • No risk of accidentally pulling malicious package updates
  • Audit trails are preserved (lockfile = provable state)
  • Consistent security scanning results

4. Performance & Reliability

  • Faster installs - no dependency resolution computation
  • Predictable timing - same install duration every time
  • Fail-fast behavior - immediate error if lockfile is inconsistent
  • Clean environment - removes existing node_modules first

When to Use Each Command

Development Workflow:

Adding/removing packages

npm install package-name npm uninstall package-name

Installing after git clone

npm install # Creates/updates lockfile if needed

Production/CI/CD:

Always use npm ci

npm ci

Troubleshooting:

When dependencies are corrupted

Shell
rm -rf node_modules package-lock.json
git checkout HEAD -- package-lock.json  # Restore known-good lockfile
npm ci                                   # Reproduce exact working state

Real-World Impact

Before npm ci (common issues):

  • "Build worked yesterday, fails today"
  • Different behavior between local and CI
  • Hard-to-debug version conflicts
  • Inconsistent performance across environments

After npm ci (benefits):

  • Predictable builds - same result every time
  • Faster CI/CD - no resolution overhead
  • Easier debugging - identical dependencies everywhere
  • Better security posture - no unexpected package updates

Best Practices

  1. Always commit package-lock.json to version control
  2. Use npm install for development (adding/removing packages)
  3. Use npm ci for production/CI/CD (exact reproduction)
  4. Never edit package-lock.json manually
  5. Regenerate lockfile when needed: rm package-lock.json npm install git add package-lock.json

The Bottom Line

npm ci ensures that your production builds use the exact same dependency tree that worked in development. It eliminates variables, surprises, and "works on my machine" issues by treating your lockfile as the definitive source of truth.

For reproducible, reliable production deployments: always use npm ci.


How to Protect Yourself from Dependency Issues

1. Use Proper npm Commands for Each Environment

Development (Local Machine):

When adding/removing packages

npm install package-name npm uninstall package-name

After git clone/pull

npm install # Only if no lockfile exists or you need to update

Production/CI/CD/Building:

ALWAYS use npm ci for builds

npm ci

Add to Your Build Scripts:

JSON
// package.json
{
  "scripts": {
    "build": "npm ci && npm run build:renderer && npm run build:electron",
    "build:production": "npm ci && cross-env NODE_ENV=production npm run build",
    "package": "npm ci && npm run build:production && electron-builder"
  }
}

2. Commit and Protect Your Lockfiles

Always Commit Lockfiles:

Make sure these are NOT in .gitignore

git add package-lock.json git add electron/package-lock.json git add renderer/package-lock.json git commit -m "Update lockfiles"

Update .gitignore Properly:

DO NOT ignore lockfiles

package-lock.json ❌ WRONG

yarn.lock ❌ WRONG

DO ignore node_modules

node_modules/ .npm-cache/

3. Use Exact Dependency Management

Pin Critical Dependencies:

JSON
// package.json - Use exact versions for critical packages
{
  "dependencies": {
    "electron": "37.2.3",           // Exact version (no ^)
    "obs-websocket-js": "5.0.6",    // Exact version
    "irc": "^0.5.2"                 // Can use ^ for stable packages
  }
}

Use .npmrc for Consistency:

.npmrc

Prefer exact versions

save-exact=true

Use specific registry

registry=https://registry.npmjs.org/

Reduce log noise

loglevel=warn

4. Implement Dependency Validation

Add Lockfile Validation to CI:

.github/workflows/build.yml (GitHub Actions example)

Shell
name: Build
on: [push, pull_request]

jobs:
  build:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v3

      # Validate lockfile hasn't changed unexpectedly
      - name: Validate lockfile
        run: |
          npm ci
          npm ci --prefix electron
          npm ci --prefix renderer

      - name: Build application
        run: npm run build:production

Pre-commit Hooks:

JSON
// package.json
{
  "husky": {
    "hooks": {
      "pre-commit": "npm ci && npm run lint && npm run build"
    }
  }
}

5. Document Your Build Process

Create CLAUDE.md or BUILD.md:

Build Instructions

Development

Shell
npm install
cd electron && npm install
cd ../renderer && npm install

Production Build

Shell
npm ci
npm run build:production

If Dependencies Break

Shell
# Restore working state
rm -rf node_modules package-lock.json
rm -rf electron/node_modules electron/package-lock.json
git checkout HEAD -- package-lock.json electron/package-lock.json
npm ci
cd electron && npm ci

Critical Commands

  • ✅ npm ci for builds
  • ❌ npm install for builds

6. Backup and Recovery Strategy

Tag Working States:

Shell
# When build is working, create a git tag
git tag -a "working-build-v1.0" -m "Known working dependency state"
git push origin working-build-v1.0

# Later, if dependencies break:
git checkout working-build-v1.0 -- package-lock.json electron/package-lock.json
npm ci && cd electron && npm ci

Backup Lockfiles:

Shell
# Before major dependency changes
cp package-lock.json package-lock.json.backup
cp electron/package-lock.json electron/package-lock.json.backup

# If something breaks
cp package-lock.json.backup package-lock.json
cp electron/package-lock.json.backup electron/package-lock.json
npm ci && cd electron && npm ci

7. Monitor for Breaking Changes

Use Dependency Scanning:

Shell
# Regular security audits
npm audit
npm audit --prefix electron
npm audit --prefix renderer

# Check for outdated packages
npm outdated

Controlled Updates:

Shell
# Update one package at a time and test
npm update package-name
npm ci && npm run build  # Test immediately

# If it works:
git add package-lock.json && git commit -m "Update package-name"

# If it breaks:
git checkout HEAD -- package-lock.json
npm ci

8. Environment Consistency

Use Node Version Manager:

Shell
# .nvmrc file
echo "18.17.0" > .nvmrc

# Use consistent Node version
nvm use

Docker for Ultimate Consistency:

Dockerfile

FROM node:18.17.0-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build:production

9. Emergency Recovery Commands

Keep these handy for when things break:

Shell
# Nuclear option - complete reset
rm -rf node_modules package-lock.json
rm -rf electron/node_modules electron/package-lock.json
rm -rf renderer/node_modules renderer/package-lock.json

# Restore from git
git checkout HEAD -- package-lock.json electron/package-lock.json renderer/package-lock.json

# Clean install
npm ci
cd electron && npm ci
cd ../renderer && npm ci

# Test build
cd .. && npm run build

10. The Golden Rule

Never run npm install in production/CI environments.

Always use:

  • npm ci for builds
  • npm install only for local development when adding/removing packages
  • Commit your lockfiles
  • Test builds after any dependency changes

This approach ensures you'll never face the same issue again because you're always using the exact dependency tree that you know works.


Ready to Fix Your Build Process?

The difference between npm install and npm ci isn't just technical - it's the difference between unpredictable deployments and reliable production builds.

Your competitors aren't dealing with "works on my machine" issues because they've already made the switch to npm ci for production.

What's stopping you from implementing reproducible builds today?

Start with one simple change: replace npm install with npm ci in your build scripts. Your future self (and your team) will thank you when deployments just work.

Welcome to the world of predictable builds 🚀

Comments

Please sign in to leave a comment.

No comments yet. Be the first to comment!