Dockerfile check · low

CMD or ENTRYPOINT in shell form

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.

$ explain Dockerfile
! CMD uses shell form low
line 8
fix: 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.

Dockerfile

Parsed in your browser. Nothing is uploaded.

Explanation

1 stage8 instructions2 low

Stage 0node:22.11-slim

  • Docs for FROM (opens in a new tab)

    Starts from the base image node, tag 22.11-slim.

    A version tag can be re-pushed to different content; only a digest is immutable. This applies to every tagged FROM in the file.

    Safer: Pin the digest as well (image:tag@sha256:<digest>) and let a bot open pull requests when it changes.

  • Docs for WORKDIR (opens in a new tab)

    Sets the working directory to /app for later instructions and the container. It is created if it does not exist.

  • Docs for COPY (opens in a new tab)

    Copies package.json, package-lock.json from your build context to ./ in the image.

  • Docs for RUN (opens in a new tab)

    Runs npm ci --omit=dev while building the image and saves the result as a new layer.

  • Docs for COPY (opens in a new tab)

    Copies src from your build context to ./src in the image.

  • Docs for USER (opens in a new tab)

    Runs later instructions and the container's process as user 10001.

  • Docs for HEALTHCHECK (opens in a new tab)

    Runs node src/healthcheck.js to check the container is healthy.

  • Docs for CMD (opens in a new tab)

    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.