Docker CMD vs ENTRYPOINT: What Runs, and What docker run Overrides
8 min readBytePatterns
Docker CMD vs ENTRYPOINT: how the two combine into one command, shell form vs exec form and PID 1, what docker run replaces, and the Kubernetes mapping.
Docker CMD vs ENTRYPOINT in one line: ENTRYPOINT is the executable the container always runs, CMD is the default arguments, and the two are joined into one command line when the container starts. Arguments after the image name in docker run replace CMD; only the --entrypoint flag replaces ENTRYPOINT. In exec form, the JSON array, the process is started directly and becomes PID 1; in shell form it runs under /bin/sh -c, which also means a shell-form ENTRYPOINT ignores CMD entirely. Kubernetes maps command onto ENTRYPOINT and args onto CMD. Facts come from the Dockerfile reference and the Kubernetes documentation listed at the end, as of October 2026.
| Dockerfile | Container runs | docker run img x y runs |
|---|---|---|
CMD ["node", "server.js"] | node server.js | x y |
ENTRYPOINT ["node", "server.js"] | node server.js | node server.js x y |
ENTRYPOINT ["node", "server.js"] + CMD ["--port", "8000"] | node server.js --port 8000 | node server.js x y |
ENTRYPOINT node server.js (shell form) | /bin/sh -c "node server.js", CMD ignored | the same; x y are dropped |
The problem it solves
Two questions come up the first week with any image. "Why does docker run myimage bash not give me a shell?" Because the image has an ENTRYPOINT, and bash became its argument. "Why does docker stop take ten seconds and then kill my server?" Because a shell-form instruction made /bin/sh PID 1, and the shell did not forward SIGTERM to the server. Both are the same topic: which instruction decides the executable, which decides the arguments, and which form starts the process directly.
The intuition
Two halves of one command line. The Dockerfile reference describes CMD as the default command or default parameters for the container, and ENTRYPOINT as the executable that runs when the container starts. When both are present in exec form, the final command is ENTRYPOINT followed by CMD. So the idiom is: fixed part in ENTRYPOINT, variable part in CMD. The image behaves like a program, and the user supplies flags.
Exec form against shell form. ["node", "server.js"] is exec form: the runtime executes node directly, it is PID 1, and signals from docker stop reach it. node server.js without brackets is shell form: the runtime runs /bin/sh -c "node server.js", the shell is PID 1, and the documentation warns that in this form the executable is not the container's PID 1 and does not receive Unix signals, so docker stop waits out the grace period and sends SIGKILL. Shell form does have one thing exec form lacks: variable expansion. CMD echo $HOME prints the home directory; CMD ["echo", "$HOME"] prints the literal string, because there is no shell to expand it. If you need both, call the shell yourself in exec form: ["sh", "-c", "echo $HOME"].
The interaction table. The reference spells out every combination, and three rows carry the surprises:
- No
ENTRYPOINT:CMDis the whole command. Exec-formCMDruns as given; shell-formCMDruns under/bin/sh -c. - Exec-form
ENTRYPOINTwith exec-formCMD: the two lists are concatenated. This is the recommended pairing. - Exec-form
ENTRYPOINTwith shell-formCMD:CMDis wrapped first, so the entrypoint receives/bin/sh -c "..."as its arguments, which is almost never what anyone wanted. - Shell-form
ENTRYPOINT: it runs under/bin/sh -c, andCMDis ignored, along with any arguments passed todocker run. - Neither: the image has no command, and running it is an error unless
docker runsupplies one.
What docker run can change. Anything after the image name replaces CMD, and only CMD. The --entrypoint flag replaces ENTRYPOINT, and the reference adds that setting ENTRYPOINT resets any CMD inherited from the base image to an empty value, so a docker run --entrypoint sh myimage lands in a shell with no arguments. Only the last CMD and the last ENTRYPOINT in a Dockerfile take effect.
The Kubernetes mapping. The Kubernetes documentation gives the same two fields under different names: command overrides the image's ENTRYPOINT, args overrides its CMD. Supplying only args keeps the image's entrypoint and replaces the default arguments, which is the usual way to pass flags. Supplying command without args drops the image's CMD, exactly like --entrypoint.
The cache rules from Docker image layers and the build cache still apply: both instructions create metadata-only layers, so changing a flag in CMD is a cheap rebuild, and putting it at the end of the Dockerfile keeps every heavy layer cached.
Watch it run
The lesson animation builds a five-layer Dockerfile and keeps every layer in the build cache. Then it edits one source file: the COPY . . layer and the step after it rebuild, while the three above come from cache. The last frames add a second FROM and show that only the final stage ships. The CMD ["node", "dist/server.js"] line in that final stage is the subject here: it is the last instruction, it changes nothing in the filesystem, and it is the only line of the whole file that decides what the container does when it starts.
Images, Layers & Multi-Stage Builds
Step 1 of 11
Each Dockerfile instruction adds a layer: an immutable set of file changes, stacked in order.
The same interactive animation as the lesson — step through it with the controls.
The code
The lesson's runtime stage, with the one line split into a fixed executable and default flags:
# illustrative: the lesson's runtime stage, CMD split into ENTRYPOINT + CMD
FROM node:22-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
ENTRYPOINT ["node", "dist/server.js"] # exec form: node is PID 1
CMD ["--port", "8000"] # default flags, replaced by docker run args
A toy model of the reference's interaction table, not the Docker daemon. Exec form is a list and is used as it is; shell form is a string and is wrapped in /bin/sh -c:
SHELL = ["/bin/sh", "-c"]
def as_argv(instruction):
"""Exec form is already a list; shell form is one string wrapped by /bin/sh -c."""
if instruction is None:
return None
return list(instruction) if isinstance(instruction, list) else SHELL + [instruction]
def resolve(entrypoint=None, cmd=None):
"""Toy model of the Dockerfile reference's ENTRYPOINT/CMD interaction table."""
if entrypoint is None and cmd is None:
return "error: no command specified"
if entrypoint is None:
return as_argv(cmd)
if isinstance(entrypoint, str): # shell-form ENTRYPOINT: CMD is ignored
return as_argv(entrypoint)
return list(entrypoint) + (as_argv(cmd) or [])
cases = [
(None, ["node", "dist/server.js"]),
(None, "node dist/server.js"),
(["node", "dist/server.js"], None),
(["node", "dist/server.js"], ["--port", "8000"]),
(["node", "dist/server.js"], "--port 8000"),
("node dist/server.js", ["--port", "8000"]),
]
for ep, cmd in cases:
print(f"ENTRYPOINT {str(ep):36} CMD {str(cmd):22} -> {resolve(ep, cmd)}")
# ENTRYPOINT None CMD ['node', 'dist/server.js'] -> ['node', 'dist/server.js']
# ENTRYPOINT None CMD node dist/server.js -> ['/bin/sh', '-c', 'node dist/server.js']
# ENTRYPOINT ['node', 'dist/server.js'] CMD None -> ['node', 'dist/server.js']
# ENTRYPOINT ['node', 'dist/server.js'] CMD ['--port', '8000'] -> ['node', 'dist/server.js', '--port', '8000']
# ENTRYPOINT ['node', 'dist/server.js'] CMD --port 8000 -> ['node', 'dist/server.js', '/bin/sh', '-c', '--port 8000']
# ENTRYPOINT node dist/server.js CMD ['--port', '8000'] -> ['/bin/sh', '-c', 'node dist/server.js']
The fifth line is the trap: a shell-form CMD under an exec-form ENTRYPOINT hands node the arguments /bin/sh -c "--port 8000". The sixth is the other trap: shell-form ENTRYPOINT discards CMD and makes the shell PID 1.
Now the overrides. docker run arguments replace CMD; --entrypoint replaces ENTRYPOINT and resets CMD. Kubernetes args and command behave the same way under new names:
def docker_run(image, args=None, entrypoint=None):
"""Toy model of `docker run [--entrypoint X] image [args]`:
args replace the image's CMD, --entrypoint replaces ENTRYPOINT and
resets the image's CMD to empty."""
ep, cmd = image["ENTRYPOINT"], image["CMD"]
if entrypoint is not None:
ep, cmd = entrypoint, None
if args:
cmd = list(args)
return resolve(ep, cmd)
image = {"ENTRYPOINT": ["node", "dist/server.js"], "CMD": ["--port", "8000"]}
print(docker_run(image)) # as built
print(docker_run(image, args=["--port", "9000", "--debug"])) # args replace CMD only
print(docker_run(image, entrypoint=["sh"])) # new ENTRYPOINT, CMD reset
print(docker_run(image, entrypoint=["sh"], args=["-c", "ls /app"]))
# ['node', 'dist/server.js', '--port', '8000']
# ['node', 'dist/server.js', '--port', '9000', '--debug']
# ['sh']
# ['sh', '-c', 'ls /app']
def k8s(image, command=None, args=None):
"""Toy model of the Kubernetes mapping: `command` overrides ENTRYPOINT,
`args` overrides CMD; a `command` without `args` drops the image's CMD."""
ep = command if command is not None else image["ENTRYPOINT"]
cmd = args if args is not None else (image["CMD"] if command is None else None)
return resolve(ep, cmd)
print(k8s(image))
print(k8s(image, args=["--port", "9000"]))
print(k8s(image, command=["node", "dist/worker.js"]))
print(k8s(image, command=["node", "dist/worker.js"], args=["--queue", "jobs"]))
# ['node', 'dist/server.js', '--port', '8000']
# ['node', 'dist/server.js', '--port', '9000']
# ['node', 'dist/worker.js']
# ['node', 'dist/worker.js', '--queue', 'jobs']
docker run image --port 9000 --debug keeps the server and changes its flags, which is the behaviour the split was designed for. docker run image bash would do the same thing, handing bash to node as a file name; the shell needs --entrypoint.
The complexity
There is no algorithmic cost here; the costs are operational:
- Shutdown time. With exec form the server receives
SIGTERMat once and can drain connections. With shell form it receives nothing, waits out the stop grace period, by default ten seconds fordocker stopand thirty for a Kubernetes Pod, and is then killed. - Rebuild cost.
CMDandENTRYPOINTlayers carry no files, so a flag change at the end of the Dockerfile is cheap and keeps the heavy layers cached. - Override friction. Every flag that lives in
ENTRYPOINTneeds--entrypointor acommandoverride to change; every flag inCMDis onedocker runargument away.
Where it goes wrong
- Shell form for the main process. The shell is PID 1 and swallows signals; every stop becomes a kill after the grace period. Use exec form, or
execthe process from a wrapper script. - Expecting
$VARin exec form. There is no shell, so nothing expands. Use["sh", "-c", "..."]when you need expansion, knowing the shell is then PID 1 unless the script ends inexec. - Shell-form
CMDunder an exec-formENTRYPOINT. The entrypoint receives/bin/sh -cas literal arguments. - Shell-form
ENTRYPOINTwith aCMD. TheCMDis silently ignored, and so aredocker runarguments. docker run image bashon an image with an entrypoint.bashbecomes an argument to the entrypoint; the shell needs--entrypoint bash.- Setting
commandin Kubernetes to pass a flag. That replaces the entrypoint and drops the image's default arguments. Flags go inargs.
When it shows up in interviews
Usually as a quick definition, then a scenario: "the container ignores SIGTERM, why?" or "how do you give the image a default argument the user can override?" The answer is the split, exec form, and what docker run replaces. In a Kubernetes round the same question arrives as command versus args. The layering that makes a flag change a cheap rebuild is in the images, layers and multi-stage builds lesson.
How to say it in an interview
"ENTRYPOINT is the executable, CMD is its default arguments, and together they form the command line. I write both in exec form, the JSON array, so the process runs directly as PID 1 and receives SIGTERM on stop; shell form puts /bin/sh -c in front, the shell becomes PID 1, and a shell-form ENTRYPOINT ignores CMD altogether. Arguments after the image name in docker run replace CMD, while --entrypoint replaces ENTRYPOINT and clears CMD. In Kubernetes, command is the entrypoint and args is the command, so to pass a flag I set args and leave command alone."
Frequently asked questions
What is the difference between CMD and ENTRYPOINT in Docker?
ENTRYPOINT sets the executable the container runs; CMD sets default arguments, or the whole command when there is no entrypoint. In exec form the two lists are concatenated. Arguments passed to docker run after the image name replace CMD; only --entrypoint replaces ENTRYPOINT. Only the last of each in a Dockerfile counts.
Should I use exec form or shell form?
Exec form, ["node", "server.js"], for the main process: it runs directly as PID 1 and receives signals, so docker stop can shut it down cleanly. Shell form runs under /bin/sh -c, which does expand variables but makes the shell PID 1 and blocks signals from reaching your process.
Can I override ENTRYPOINT with docker run?
Yes, with the --entrypoint flag, which replaces the image's entrypoint and resets its CMD to empty; arguments after the image name then become the new command's arguments. Plain arguments after the image name do not touch the entrypoint, they only replace CMD. In Kubernetes the equivalent is the container's command field.
Sources
- Dockerfile reference: CMD — Docker Docs
- Dockerfile reference: ENTRYPOINT — Docker Docs
- Dockerfile reference: Shell and exec form — Docker Docs
- docker container run — Docker Docs
- Define a Command and Arguments for a Container — Kubernetes Documentation