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.
Both instructions accept two syntaxes:
CMD node server.js # shell form: runs /bin/sh -c "node server.js"
CMD ["node", "server.js"] # exec form: runs node directly as PID 1In 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.
docker run -d --name t1 my-app # shell form image
time docker stop t1 # ~10 s: SIGTERM never reached the appIf 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.
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.
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:.
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:
#!/bin/sh
set -e
# Run migrations or render config templates here
if [ "$1" = "node" ]; then
echo "Starting application on port ${PORT:-3000}"
fi
exec "$@"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"]).
CMD node server.js and wondering why docker stop takes ten seconds every time.$HOME to expand inside CMD ["echo", "$HOME"]; exec form performs no substitution.ENTRYPOINT shell form and then trying to pass arguments, which are silently discarded.ENTRYPOINT for an image that should run arbitrary commands, forcing every user to add --entrypoint.Why does a container defined with `CMD node server.js` ignore `docker stop` until the timeout expires?
SIGTERM.ENTRYPOINT is the fixed executable; CMD supplies default arguments that run-time arguments replace.CMD alone for general-purpose images and ENTRYPOINT plus CMD for tool-like images.docker run image args (replaces CMD) or --entrypoint (replaces ENTRYPOINT).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.