Use CMD with the node binary in exec form, such as CMD ["node", "server.js"], rather than npm start. Running through npm adds an extra process between Docker and your app, and the Node.js image maintainers note that signals such as SIGTERM can be swallowed by npm instead of reaching your code.
Why signals matter for security and reliability
When Kubernetes or Docker stops a container, it sends SIGTERM and waits for a grace period before force killing. Your app uses that window to finish requests, close database connections and flush logs. If the signal never arrives, the container is killed hard.
Hard kills are mostly a reliability problem. They become a security problem when shutdown hooks do cleanup that matters, such as revoking a short lived credential or flushing an audit log line.
What the Node.js image docs say
The official guidance from the Node.js Docker maintainers makes two points relevant here.
First, baking the command into the image with node as CMD reduces the number of running processes, and it lets SIGTERM and SIGINT reach the Node.js process instead of being swallowed by npm.
Second, Node.js was not designed to run as PID 1. A Node.js process running as PID 1 will not respond to SIGINT and similar signals in the default way, so they recommend wrapping it with a small init program. Docker has offered an init flag on docker run since version 1.13, and you can also bake an init such as Tini or dumb-init into the image.
The pattern to use
FROM node:24-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]
Then either run with the init flag, or add an init in the image and start through it. The maintainers' multi-stage example does the second, with a CMD of dumb-init followed by node and the script path.
Shell form versus exec form
| Form | Example | Effect |
|---|---|---|
| Exec form | CMD ["node", "server.js"] | Node is the direct child, receives signals |
| Shell form | CMD node server.js | Wrapped in a shell, signal delivery depends on the shell |
| npm script | CMD ["npm", "start"] | npm sits in between, may swallow signals |
Exec form is the JSON array with double quotes. A missing quote turns it into shell form silently in some cases, so check the built image with docker inspect.
Try it yourself
Build the image, start it, then run docker stop and time how long it takes. A clean shutdown returns in well under the default ten second grace period. If it takes the full ten seconds, your process did not receive the signal and you are being force killed.
Add a handler in code too. Node.js emits signal events on the process object, so an on SIGTERM handler can close the server and exit.
Where Safeguard fits
This is a runtime hygiene point rather than a finding. Safeguard focuses on what your code and dependencies contain, using dependency scanning, reachability analysis and SBOMs, with policy gates in pull requests. A free scan shows what ships in your Node.js service.
Quick check
Does an init process replace graceful shutdown code?
No. An init forwards signals and reaps zombie processes. Your application still needs a handler that closes servers and connections when SIGTERM arrives.
Is the init flag enough in Kubernetes?
The Docker init flag is a docker run option. In Kubernetes, put an init such as Tini or dumb-init in the image and start through it, or confirm your runtime and process model handle signals for your entrypoint.
Sources
Last reviewed: 19 October 2026.