Safeguard
Developer Tools•

Should a Node.js Dockerfile use npm start or node as CMD?

Use node directly as CMD in exec form. npm start adds a process that can swallow SIGTERM, and PID 1 handling needs care. What to set and how to test it.

Safeguard Security Team
4 min read

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

FormExampleEffect
Exec formCMD ["node", "server.js"]Node is the direct child, receives signals
Shell formCMD node server.jsWrapped in a shell, signal delivery depends on the shell
npm scriptCMD ["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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.