Dockerfile CMD vs ENTRYPOINT
CMD vs ENTRYPOINT. Exec vs shell form, how runtime arguments override each, using them together, and why signals get ignored.
Both define what runs when a container starts, and the difference between them is one of the most frequently asked Docker questions.
The short version:
ENTRYPOINT defines what runs. CMD defines the default arguments.
Everything else follows from how each behaves when you pass arguments to docker run.
The behavioural difference
With docker run myimage extra args | |
|---|---|
| CMD | Replaced entirely by extra args |
| ENTRYPOINT | extra args are appended to it |
That is the whole distinction. A worked example:
FROM alpine
CMD ["echo", "hello"]
docker run myimage # → hello
docker run myimage echo goodbye # → goodbye (CMD replaced)
FROM alpine
ENTRYPOINT ["echo", "hello"]
docker run myimage # → hello
docker run myimage goodbye # → hello goodbye (appended)
Using them together
This is the idiomatic pattern, and it is what most well-built images do:
FROM python:3.12-slim
ENTRYPOINT ["python"]
CMD ["app.py"]
docker run myimage # → python app.py
docker run myimage other.py # → python other.py
ENTRYPOINT fixes the executable; CMD supplies a default argument the user can override. The container always runs Python, but which script is up to the caller.
Another common shape. A tool with a sensible default subcommand:
ENTRYPOINT ["curl"]
CMD ["--help"]
Exec form vs shell form
Both instructions accept two syntaxes, and choosing wrongly causes a real and confusing bug.
CMD ["node", "server.js"] # exec form. JSON array
CMD node server.js # shell form
| Exec form | Shell form | |
|---|---|---|
| Runs as | The binary directly, PID 1 | /bin/sh -c "your command" |
| Receives signals | Yes | No (sh gets them, not your process) |
Shell features ($VAR, &&, pipes) | No | Yes |
| Requires a shell in the image | No | Yes |
Use exec form. The signal behaviour is the reason, and it is not a small detail.
Why shell form breaks graceful shutdown
With shell form, /bin/sh is PID 1 and your application is a child process. docker stop sends SIGTERM to PID 1, the shell, which does not forward it. Your application never learns it should shut down.
Docker waits 10 seconds, then SIGKILLs everything.
The symptoms are recognisable: containers always take exactly 10 seconds to stop, in-flight requests are cut off, and graceful shutdown code never runs. On Kubernetes the same problem makes every rolling deploy drop connections.
# Broken, sh is PID 1, your app never sees SIGTERM
CMD node server.js
# Correct, node is PID 1 and receives signals
CMD ["node", "server.js"]
When you genuinely need the shell
Variable expansion and shell operators only work in shell form:
# Exec form does NOT expand $PORT. It passes the literal string
CMD ["node", "server.js", "--port", "$PORT"] # wrong
# Shell form expands it, but loses signal handling
CMD node server.js --port $PORT # works, but PID 1 is sh
The fix that gives you both is exec, which replaces the shell rather than spawning a child:
CMD ["sh", "-c", "exec node server.js --port $PORT"]
exec means the shell process is replaced by node, so node becomes PID 1 and receives signals normally.
Entrypoint scripts
The standard pattern for images needing setup before the main process:
#!/bin/sh
set -e
# Setup: wait for dependencies, run migrations, render config
until nc -z "$DB_HOST" 5432; do
echo "waiting for database"
sleep 1
done
# Hand over. Exec replaces this script, so the app becomes PID 1
exec "$@"
COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["node", "server.js"]
Two details make this work. exec "$@" runs whatever CMD supplied, replacing the script so the application ends up as PID 1 with working signal handling. And because CMD is still overridable, docker run myimage sh drops you into a shell while still running the setup first.
Omitting exec, writing just "$@", is the single most common bug in entrypoint scripts. The script stays PID 1, signals go to it, and graceful shutdown breaks.
Overriding at runtime
docker run myimage other-arg # overrides CMD
docker run --entrypoint /bin/sh myimage # overrides ENTRYPOINT
--entrypoint is how you get a shell in an image whose entrypoint immediately runs something else. Invaluable for debugging a container that exits instantly.
Note the asymmetry: --entrypoint takes only the executable. Arguments to it go after the image name.
Inheritance
A child image's ENTRYPOINT resets the inherited CMD to null. If you set ENTRYPOINT in a derived image and rely on the base image's CMD, you get nothing:
FROM someimage # has ENTRYPOINT ["app"] and CMD ["--serve"]
ENTRYPOINT ["wrapper"]
# CMD is now empty. You must restate it
CMD ["--serve"]
Choosing
| Situation | Use |
|---|---|
| Image is one specific tool | ENTRYPOINT + CMD for defaults |
| Image is a general environment | CMD only, so it is easily overridden |
| Setup needed before the app | ENTRYPOINT script ending in exec "$@" |
| Anything at all | Exec form, always |
Quick reference
| Task | Syntax |
|---|---|
| Fixed executable | ENTRYPOINT ["node"] |
| Default arguments | CMD ["server.js"] |
| Shell features safely | CMD ["sh", "-c", "exec app --port $PORT"] |
| Override the command | docker run <image> <cmd> |
| Override the entrypoint | docker run --entrypoint <bin> <image> |
| Inspect both | docker inspect --format '{{.Config.Entrypoint}} {{.Config.Cmd}}' <image> |
Key takeaways
- ENTRYPOINT is what runs; CMD is the default arguments.
- Runtime args replace CMD but are appended to ENTRYPOINT.
- Always use exec form: the JSON array syntax.
- Shell form makes
/bin/shPID 1, so your app never receivesSIGTERMand every stop takes the full 10-second timeout. CMD ["sh", "-c", "exec app $VAR"]gives variable expansion and correct signal handling.- Entrypoint scripts must end in
exec "$@", or the script stays PID 1. --entrypointoverrides the entrypoint, which is how you debug a container that exits immediately.- Setting ENTRYPOINT in a child image nulls the inherited CMD.
Check your understanding
Containers that always take 10 seconds to stop
A Dockerfile uses CMD node server.js in shell form. Every docker stop takes exactly 10 seconds and the application's shutdown handler never runs. Why?
Predicting the command
An image has ENTRYPOINT ["python"] and CMD ["app.py"]. What runs for docker run myimage other.py?
Frequently Asked Questions
What is the difference between CMD and ENTRYPOINT in Docker?
ENTRYPOINT defines the executable that always runs; CMD defines the default arguments passed to it. The practical difference shows at runtime: arguments given to docker run replace CMD entirely but are appended to ENTRYPOINT. So an image with ENTRYPOINT ["python"] and CMD ["app.py"] runs python app.py by default and python other.py when you pass other.py. The executable is fixed and the caller controls the arguments.
Should I use CMD or ENTRYPOINT?
Use both together for an image that is one specific tool: ENTRYPOINT fixes the binary, CMD provides overridable defaults. Use CMD alone for a general-purpose image where users may want to run arbitrary commands, such as a language runtime image. Use an ENTRYPOINT script ending in exec "$@" when setup must happen before the main process. Waiting for a dependency, rendering configuration, running migrations.
What is the difference between exec form and shell form?
Exec form is the JSON array syntax, CMD ["node", "server.js"], and runs the binary directly as PID 1. Shell form, CMD node server.js, runs it via /bin/sh -c, making the shell PID 1 with your application as a child. Exec form is strongly preferred because only PID 1 receives signals, with shell form your application never sees SIGTERM and graceful shutdown silently never happens. Shell form does allow variable expansion and pipes, which exec form does not.
Why does my container take 10 seconds to stop?
Because your application is not receiving SIGTERM, so docker stop waits the full grace period and then SIGKILLs it. The usual cause is shell form: /bin/sh is PID 1 and does not forward signals to its child. Switch to exec form so your process is PID 1. If you have an entrypoint script, make sure it ends with exec "$@" rather than plain "$@", since without exec the script remains PID 1 and absorbs the signal.
How do I use an environment variable in CMD?
Exec form does not expand variables, CMD ["node", "app.js", "--port", "$PORT"] passes the literal string $PORT. The safe way to get expansion without losing signal handling is CMD ["sh", "-c", "exec node app.js --port $PORT"]. The exec is essential: it replaces the shell process with your application so it becomes PID 1. Plain shell form expands the variable but leaves the shell as PID 1 and breaks shutdown.
How do I override ENTRYPOINT to debug a container?
docker run --entrypoint /bin/sh -it myimage replaces the entrypoint and drops you into a shell, which is the standard way to inspect an image whose entrypoint exits immediately or crashes. Note that --entrypoint accepts only the executable. Any arguments go after the image name. For a distroless image with no shell at all, copy the filesystem out with docker create and docker cp instead.