-
Notifications
You must be signed in to change notification settings - Fork 251
Description
Motivation ("The Why")
npm run spawns scripts through a shell (/bin/sh -c), creating an unnecessary intermediate process that interferes with signal propagation. This causes Node.js applications to terminate abruptly without completing their cleanup procedures when receiving SIGTERM/SIGINT signals.
This is a fundamental Unix process management issue that affects:
- Docker containers (not just Kubernetes)
- Systemd services
- Process managers (PM2, Forever, etc.)
- CI/CD environments
- Any production deployment using
npm run
The problem: When a termination signal is sent, the shell may exit immediately without waiting for its child process, causing npm to exit prematurely and killing the Node.js application before graceful shutdown completes.
Example
Consider a basic Node.js server with cleanup logic:
// server.js
const server = require('http').createServer();
process.on('SIGTERM', () => {
console.log('Graceful shutdown started');
server.close(() => {
console.log('Cleanup complete');
process.exit(0);
});
});
server.listen(3000);Problem occurs in any of these scenarios:
- Docker container:
docker stop <container> - Systemd service:
systemctl stop myapp - Process manager:
pm2 stop app - Manual termination:
kill <pid>
In all cases, if started with npm run start, the graceful shutdown may not complete because the shell intermediary disrupts signal propagation.
How
Add an opt-in flag to control how npm run spawns processes, eliminating the problematic shell intermediary when not needed.
Current Behaviour
# Current process tree with npm run
npm → /bin/sh -c "node server.js" → node server.js
↓ ↓
SIGTERM (shell may exit without waiting)
↓
(npm exits because child is gone)
(node process terminated abruptly)The shell (/bin/sh) behavior varies:
- Some shells (ash, dash) exit immediately on SIGTERM
- This causes npm to detect child exit and terminate
- Node.js process is killed before cleanup completes
Current workaround requires modifying code:
{
"scripts": {
"start": "exec node server.js" // Must add exec manually
}
}This workaround is:
- Not documented clearly
- Not obvious to developers
- Required in every project
- Easy to forget
Desired Behaviour
Provide an opt-in mechanism that works without code changes:
Option 1 - Environment variable (for deployments):
NPM_PREFER_DIRECT_EXEC=1 npm run startOption 2 - CLI flag (for specific runs):
npm run start --prefer-direct-execOption 3 - Config file (for projects):
# .npmrc
prefer-direct-exec=trueWhen enabled:
- Simple commands: Spawn directly without shell
- Complex commands (with pipes, &&, etc.): Auto-prepend
execto replace shell - Windows: No change (different process model)
- Default: Current behavior (backward compatible)
Result:
# With flag enabled
npm → node server.js (direct, no shell)
↓ ↓
SIGTERM (node receives signal)
↓ ↓
(waits) (graceful shutdown completes)References
- Related to [Feature] Opt-in to make
npm runwait for the actual app process (direct exec / exec-replace) for graceful shutdown on Kubernetes run-script#237 (implementation tracking) - Related to [Feature] Expose
--prefer-direct-execfornpm runand add a production note (tracking; blocked by @npmcli/run-script https://github.com/npm/run-script/issues/237) cli#8509 (original feature request)