CMD vs ENTRYPOINT
What actually runs when a container starts.
A container has to be told which single program to run the moment it starts, and nothing else about it matters until that is settled. Quick vocabulary first, because these three words turn up in every sentence below. A container is one running copy of your app with everything the app needs packed in around it. It starts from an image, a frozen bundle of that app and its surroundings, ready to ship anywhere. The image is built from a Dockerfile, a plain text recipe Docker reads line by line. Two lines near the end of that recipe decide which program the container actually starts: CMD and ENTRYPOINT. Beginners swap them constantly. Get them backwards and your container runs the wrong thing, or silently drops the arguments you typed, or hangs for a full ten seconds every time you try to stop it. Here is how to tell them apart for good.
A drip coffee machine has one job. It makes coffee. It will never make toast, no matter what you press. That fixed job is ENTRYPOINT, the thing the container always is. The same machine has a cup-size dial with a default setting printed on it, and you can turn that dial before you hit start. That changeable default is CMD. ENTRYPOINT is the program that always runs. CMD is the input you can swap at the last second. Hold that picture and the two Dockerfile lines stop feeling like a coin flip.
CMD: the default anyone can overrule
CMD sets a default command for the container, and defaults exist to be argued with. Anything you type after the image name on docker run throws the default out and runs your text instead. Here is a tiny image built on Alpine, a stripped-down version of Linux that many images start from because it weighs around 7 megabytes. The echo program prints back whatever text you hand it, which makes it perfect for watching who wins.
FROM alpine:3.20CMD ["echo", "Hello from CMD"]
docker build -t greeter .docker run greeter
[+] Building 0.8s (5/5) FINISHED=> [internal] load build definition from Dockerfile 0.0s=> [internal] load metadata for docker.io/library/alpine:3.20 0.3s=> CACHED [1/1] FROM docker.io/library/alpine:3.20 0.0s=> exporting to image 0.0s=> => naming to docker.io/library/greeter 0.0sHello from CMD
docker run greeter echo "Goodbye instead"
Goodbye instead
The default, echo Hello from CMD, is gone. Your echo Goodbye instead ran in its place. That is the whole personality of CMD. It is a suggestion, and the person running the container gets the last word. So CMD is where you put a sensible default that people will often want to replace.
ENTRYPOINT: the program the container always runs
ENTRYPOINT plays by the opposite rule. It names a fixed program that runs every single time, and whatever you type after the image name gets handed to that program as extra arguments rather than replacing it. Same base image, one line changed.
FROM alpine:3.20ENTRYPOINT ["echo", "greeting:"]
docker build -t greeter2 .docker run greeter2 hello there
greeting: hello there
The words hello there replaced nothing. They rode along as two more arguments to echo. So how do you run something completely different from this image? There is exactly one lever: the --entrypoint flag, which swaps the fixed program at run time. Leave it off and the ENTRYPOINT program is locked in.
docker run --entrypoint date greeter2
Thu Jul 16 10:32:14 UTC 2026
One trap catches almost everyone. Docker never checks during the build that the program you named in the brackets actually exists. The build succeeds, you feel good about yourself, and then the container refuses to start. Here is that image, where greet is not a real command.
FROM alpine:3.20ENTRYPOINT ["greet"]CMD ["World"]
docker build -t greeter3 .docker run greeter3
docker: Error response from daemon: failed to create task for container:failed to create shim task: OCI runtime create failed: runc create failed:unable to start container process: exec: "greet": executable file not foundin $PATH: unknown.Run 'docker run --help' for more information.
Read the last useful part first: exec: greet: executable file not found in $PATH. Docker took your ENTRYPOINT at its word and went looking for a program called greet on the container's PATH, the list of folders the system searches when it needs to find a program. Nothing there by that name. Two ways out. Point ENTRYPOINT at a program that really exists, like echo, or install the tool in an earlier build step so it lands on the PATH before the container ever starts.
Both together: the pattern you will actually ship
Most real Dockerfiles use both lines. ENTRYPOINT names the program, CMD supplies the default arguments, and whoever runs the image can replace those arguments without touching the program. The container starts behaving like a proper command-line tool. Run it bare and it does something sensible. Pass arguments and they flow straight through to the program.
FROM alpine:3.20ENTRYPOINT ["echo", "greeting:"]CMD ["hello", "world"]
docker build -t greeter4 .docker run greeter4docker run greeter4 goodbye
greeting: hello worldgreeting: goodbye
With nothing after the image name, CMD filled in hello world. With goodbye, your argument replaced the CMD default and went straight to echo. ENTRYPOINT never budged either time. That is why the split is so popular: the fixed job lives in ENTRYPOINT, the swappable bits live in CMD.
One case the coffee machine leaves out: a Dockerfile with CMD and no ENTRYPOINT at all. Then CMD is the program itself, not a list of arguments, which is why CMD ["node", "server.js"] works perfectly well on its own. Add an ENTRYPOINT line later and that same CMD quietly changes meaning, from the program to a bundle of arguments handed to somebody else's program. That silent shift is behind most of the confused forum threads on this topic.
Exec form matters most for long-running services. Written as CMD ["node", "server.js"], a JSON array (JavaScript Object Notation, a plain-text way of writing a list in square brackets), your app sits at process 1 and receives SIGTERM (the polite 'please wrap up and exit' signal) the instant you run docker stop. Written as CMD node server.js, /bin/sh -c gets that signal instead and your app never hears a thing. Every shutdown step you carefully wrote, flushing logs, finishing the request in flight, closing the database connection, quietly stops happening.
The shape you will see in production is ENTRYPOINT ["app"] paired with CMD ["--help"] or a set of default flags. Run docker run image --flag and your flag replaces the CMD defaults while the entrypoint stays exactly where it was. docker run image other-command swaps the arguments the same way. Only --entrypoint replaces the program itself, which is a gift while debugging (run sh instead of the app and look around inside) and a hazard when someone leaves it buried in a script and nobody notices the real app stopped running.
Exit codes tell you which half of the problem you have. A container that exits instantly with code 0 probably ran a one-shot command and finished, exactly as instructed. Code 127 means the executable was not found, so your ENTRYPOINT path is wrong or the binary was never installed in the image. Code 1 means the program did start and then failed on its own terms, so go read the logs instead of the Dockerfile. Match the symptom to the code before you start rewriting lines at random.
Plenty of official base images ship their own entrypoint script. postgres and nginx both do. The moment you write FROM postgres, you inherit that script, and your CMD line is no longer the program, only arguments fed into someone else's startup logic. Read the base image's documentation before assuming your CMD runs the show. That one check saves you the week people otherwise spend asking why their command is being ignored.
The fastest way to make all this stick is throwaway Alpine images that do nothing but echo. Build one with CMD alone, one with ENTRYPOINT alone, then one with both, and run each twice: once bare, once with extra words tacked on the end. Ten minutes of that beats any amount of reading, and it saves you the afternoon you would otherwise lose when a CI pipeline (continuous integration, the automated system that builds and tests your code on every push) passes arguments that an entrypoint script quietly eats.
Try this
Build a small image that uses ENTRYPOINT and CMD together, override the arguments at run time, then override the entrypoint itself so you can poke around inside.
printf 'FROM alpine:3.20\nENTRYPOINT ["echo","log:"]\nCMD ["idle"]\n' > Dockerfile.epdocker build -t epdemo -f Dockerfile.ep .docker run --rm epdemodocker run --rm epdemo starteddocker run --rm --entrypoint printenv epdemo PATH | head -c 40; echo
[+] Building ... naming to docker.io/library/epdemolog: idlelog: started/usr/local/sbin:/usr/local/bin:/usr/sbin:...
Takeaway
ENTRYPOINT is what the container is, CMD is what it says by default, and arguments on docker run only ever fight with CMD. Write both as bracketed lists so the stop signal reaches your program instead of a shell.