CMD vs ENTRYPOINT

What actually runs when a container starts.

Beginner10 min · lesson 11 of 16
In plain terms
ENTRYPOINT is the machine itself — say, a blender — that the container always is. CMD is the default speed setting printed on the dial, which you can override when you press start. Together: a blender preset to “smoothie” that you can switch to “crush ice” on the spot.

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.

Dockerfile
FROM alpine:3.20
CMD ["echo", "Hello from CMD"]
bash
docker build -t greeter .
docker run greeter
Output
[+] 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.0s
Hello from CMD
bash
docker run greeter echo "Goodbye instead"
Output
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.

Dockerfile
FROM alpine:3.20
ENTRYPOINT ["echo", "greeting:"]
bash
docker build -t greeter2 .
docker run greeter2 hello there
Output
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.

bash
docker run --entrypoint date greeter2
Output
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.

Dockerfile
FROM alpine:3.20
ENTRYPOINT ["greet"]
CMD ["World"]
bash
docker build -t greeter3 .
docker run greeter3
Output
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 found
in $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.

Dockerfile
FROM alpine:3.20
ENTRYPOINT ["echo", "greeting:"]
CMD ["hello", "world"]
bash
docker build -t greeter4 .
docker run greeter4
docker run greeter4 goodbye
Output
greeting: hello world
greeting: 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.

Use the bracket form, or docker stop will hang for ten seconds
Write CMD and ENTRYPOINT as a bracketed list, like ["echo", "greeting:"], never as a bare line like ENTRYPOINT echo greeting:. The bracketed version is called exec form, and it runs your program directly as process number 1, the first and most important process inside the container. Process 1 is the one that receives the stop signal, so your program hears docker stop and exits cleanly. The bare version is called shell form, and it wraps your program inside /bin/sh, a small built-in shell. The shell catches the stop signal and never passes it along. Docker waits ten seconds, gives up, and kills the container hard. Shell-form ENTRYPOINT also throws away CMD and any arguments you add on docker run, without a word of warning.
What runs when you type docker run
What runs when you type docker run

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.

terminal
printf 'FROM alpine:3.20\nENTRYPOINT ["echo","log:"]\nCMD ["idle"]\n' > Dockerfile.ep
docker build -t epdemo -f Dockerfile.ep .
docker run --rm epdemo
docker run --rm epdemo started
docker run --rm --entrypoint printenv epdemo PATH | head -c 40; echo
output
[+] Building ... naming to docker.io/library/epdemo
log: idle
log: 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.

Quick check
01A Dockerfile ends with ENTRYPOINT ["echo", "log:"] and CMD ["idle"]. You run docker run myimg started. What lands on your screen?
Incorrect — That is what a bare docker run myimg gives you. You passed started, which pushes the CMD default idle out of the way.
Correct — Your argument started takes the place of the CMD default, then gets tacked onto the ENTRYPOINT, so echo prints log: started.
Incorrect — The ENTRYPOINT echo log: runs every time, and a plain argument cannot shake it off. Changing the program takes --entrypoint.
Incorrect — CMD does not stack alongside your argument. The run argument wipes CMD out completely, so echo only ever sees started.
02The lesson tells you to write ENTRYPOINT as a bracketed list like ["echo", "greeting:"] rather than the bare line ENTRYPOINT echo greeting:. What makes the bare (shell) form stall docker stop for ten seconds?
Incorrect — That has it the wrong way round. The bracket (exec) form is the one that puts your program at process 1, where it receives the signal properly.
Incorrect — Size has nothing to do with it. The delay is purely about which process gets handed the stop signal.
Correct — /bin/sh sits at process 1, swallows the signal, and ten seconds later Docker force-kills the container.
Incorrect — Nothing retries. Those ten seconds are Docker's fixed grace period before it force-kills a process that ignored the stop signal.
03Your Dockerfile has FROM alpine:3.20 and ENTRYPOINT ["greet"], where greet is not a real program. You run docker build, then docker run. How does it play out?
Incorrect — The build never goes looking for the program. It finishes happily, and the trouble only shows up at run time.
Correct — Docker takes ENTRYPOINT at its word and searches the PATH only when the container starts, so the build passes and the start fails.
Incorrect — With no real program to run, the container never starts at all, so there is nothing for it to print.
Incorrect — There is no fallback shell. A missing ENTRYPOINT program stops the container dead before it starts.

Related