CMD node server.js (shell form) is run as /bin/sh -c 'node server.js', so the shell is PID 1 and your app is its child. The shell does not forward SIGTERM, so docker stop waits out its timeout (10 seconds by default) and then kills the app with SIGKILL: no graceful shutdown, no draining connections. The exec form, CMD ["node", "server.js"], runs the app directly as PID 1.
openrouting — dockerfile explainer
$ explain Dockerfile! CMD uses shell form lowline 8fix: see below$
What the free Dockerfile explainer reports on the risky example below.
Why it matters
Graceful shutdown depends on the app receiving SIGTERM. With shell form it doesn't, so in-flight requests are cut off and every stop or deploy takes the full timeout.
Shell form ENTRYPOINT also ignores CMD and any arguments passed to docker run, which surprises people who expect to pass flags to the image.
The explainer flags shell-form CMD and ENTRYPOINT in the final stage only, since that is the stage that runs.
Risky and safer Dockerfile
RiskyDockerfile
FROM node:22.11-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY src ./src
USER 10001
HEALTHCHECK CMD ["node", "src/healthcheck.js"]
CMD node src/server.js
SaferDockerfile
FROM node:22.11-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY src ./src
USER 10001
HEALTHCHECK CMD ["node", "src/healthcheck.js"]
CMD ["node", "src/server.js"]
How to fix it
Write CMD and ENTRYPOINT as JSON arrays with double quotes: CMD ["node", "src/server.js"].
If you need shell features such as variable expansion, use a small entrypoint script that ends with exec "$@" or exec node src/server.js, so the app replaces the shell.
Run with docker run --init (or add tini) if your process starts child processes it doesn't reap, and make sure the app handles SIGTERM.
When it's acceptable
A shell-form command that starts with exec (CMD exec node server.js) replaces the shell, so signals reach the app. The explainer still flags it because it only checks the form.
Short-lived jobs where shutdown behaviour doesn't matter.
Check your own Dockerfile
The risky example is loaded below. Paste your own file to check it for this and every other Dockerfile rule. It runs in your browser; nothing is uploaded.
Sets the default command to node src/server.js. It runs when the container starts, and anything passed to docker run replaces it.
Shell form runs under /bin/sh -c, so your process isn't PID 1 and doesn't receive SIGTERM: docker stop waits out the timeout and then kills it with no graceful shutdown.
Safer: Use the exec form, e.g. CMD ["python", "app.py"], and add --init (or tini) if your process starts children it doesn't reap.
Questions
What is the difference between shell form and exec form CMD?
Shell form (CMD node app.js) runs through /bin/sh -c; exec form (CMD ["node", "app.js"]) runs the program directly without a shell.
Why does docker stop take 10 seconds?
Usually because the app never received SIGTERM, often due to shell-form CMD, so Docker waits for its timeout and then sends SIGKILL.
Does exec form CMD expand environment variables?
No. Exec form doesn't run a shell, so $VAR is passed literally. Use an entrypoint script or ["sh", "-c", "exec …"] if you need expansion.