CMD vs ENTRYPOINT: Shell Form, Exec Form and Overrides

Intermediate
11 min

CMD vs ENTRYPOINT: Shell Form, Exec Form and Overrides

CMD and ENTRYPOINT both define what runs when a container starts, and the confusion between them causes real bugs: containers that ignore docker stop, arguments that vanish, and images that cannot be reused. After this lesson you will know how the two instructions combine, why the JSON exec form matters for signals, and how to write an entrypoint script that stays correct.

Shell Form vs Exec Form

Both instructions accept two syntaxes:

dockerfile
CMD node server.js # shell form: runs /bin/sh -c "node server.js" CMD ["node", "server.js"] # exec form: runs node directly as PID 1

In shell form, Docker wraps the command in /bin/sh -c. The shell becomes PID 1 and your program is its child. That has two consequences: environment variables like $PORT are expanded, which is convenient, but signals sent by docker stop go to the shell, which does not forward them. The container then waits the full grace period and is killed.

In exec form, the JSON array is executed directly. Your program is PID 1, receives SIGTERM and can shut down gracefully. Variable expansion does not happen, because there is no shell. Exec form is the recommended default for both CMD and ENTRYPOINT.

bash
docker run -d --name t1 my-app # shell form image time docker stop t1 # ~10 s: SIGTERM never reached the app

If you need a variable and exec form, call the shell explicitly: CMD ["sh", "-c", "node server.js --port $PORT"], or better, read the variable inside the application.

How CMD and ENTRYPOINT Combine

ENTRYPOINT is the executable; CMD provides default arguments to it. Arguments passed to docker run image ... replace CMD but are appended to ENTRYPOINT.

| Dockerfile | docker run img runs | docker run img --debug runs | |------------|----------------------|-------------------------------| | CMD ["node","app.js"] | node app.js | --debug (fails: not a program) | | ENTRYPOINT ["node","app.js"] | node app.js | node app.js --debug | | ENTRYPOINT ["node","app.js"] + CMD ["--port","3000"] | node app.js --port 3000 | node app.js --debug |

This is why CLI-style images (docker run --rm curlimages/curl https://example.com) use ENTRYPOINT: the image behaves like the tool itself. Images that are meant to be started with different commands (docker run node:22 npm test) use only CMD.

Note that if ENTRYPOINT is in shell form, CMD and run-time arguments are ignored entirely, another reason to use exec form.

Overriding at Run Time

bash
docker run --rm my-app --port 9000 # replaces CMD docker run --rm --entrypoint sh -it my-app # replaces ENTRYPOINT; CMD is dropped docker run --rm --entrypoint "" my-app ls / # clear the entrypoint completely docker inspect --format '{{.Config.Entrypoint}} {{.Config.Cmd}}' my-app

--entrypoint is the standard trick for getting a shell into an image whose entrypoint refuses to start, for example a database image with a missing password variable. In Compose the equivalent keys are entrypoint: and command:.

Writing an Entrypoint Script

Many official images use a small shell script as the entrypoint to prepare the environment before handing over to the real process. The essential pattern is exec "$@", which replaces the shell with the final command so it becomes PID 1:

bash
#!/bin/sh set -e # Run migrations or render config templates here if [ "$1" = "node" ]; then echo "Starting application on port ${PORT:-3000}" fi exec "$@"
dockerfile
COPY --chmod=755 docker-entrypoint.sh /usr/local/bin/ ENTRYPOINT ["docker-entrypoint.sh"] CMD ["node", "server.js"]

Without exec, the shell stays as PID 1, the application runs as a child, and the signal problem returns. Also remember that PID 1 on Linux does not reap orphaned child processes automatically; if your application spawns subprocesses, start it under a minimal init such as tini (docker run --init or ENTRYPOINT ["tini", "--", "node", "server.js"]).

Common Mistakes

  • Writing CMD node server.js and wondering why docker stop takes ten seconds every time.
  • Expecting $HOME to expand inside CMD ["echo", "$HOME"]; exec form performs no substitution.
  • Putting the whole command in ENTRYPOINT shell form and then trying to pass arguments, which are silently discarded.
  • Using ENTRYPOINT for an image that should run arbitrary commands, forcing every user to add --entrypoint.
Quick Quiz
Question 1 of 3

Why does a container defined with `CMD node server.js` ignore `docker stop` until the timeout expires?

Key Takeaways

  • Prefer the exec (JSON array) form so your process is PID 1 and receives SIGTERM.
  • ENTRYPOINT is the fixed executable; CMD supplies default arguments that run-time arguments replace.
  • Use CMD alone for general-purpose images and ENTRYPOINT plus CMD for tool-like images.
  • Override with docker run image args (replaces CMD) or --entrypoint (replaces ENTRYPOINT).
  • Entrypoint scripts must end with exec "$@"; add tini when the app spawns child processes.

Next lesson: ARG, ENV, HEALTHCHECK and USER — parameterize builds, configure runtime, report health and drop root privileges.

CMD vs ENTRYPOINT: Shell Form, Exec Form and Overrides - Docker | CodeYourCraft | CodeYourCraft