Chapter 4: Dockerfiles
Topic 24

CMD vs ENTRYPOINT

InstructionRuntime

This is the most-confused pair in the whole Dockerfile, and getting it wrong produces containers that ignore your arguments, swallow signals, or refuse to take a command at all. The clean mental model: ENTRYPOINT is the fixed executable the container always runs, and CMD is the default arguments handed to it, which docker run can override.

For Driftwood, ENTRYPOINT ["gunicorn"] plus CMD ["--bind", "0.0.0.0:8000", "app:app"] means the container always runs gunicorn with overridable defaults — and the exec-vs-shell choice from topic 22 decides whether gunicorn is PID 1 and answers your docker stop. The two instructions and the form choice together decide what runs, how it is tuned, and whether it shuts down cleanly.

ENTRYPOINT — The Fixed Executable

ENTRYPOINT sets the program the container runs, and it is not replaced by arguments to docker run — those become its arguments instead. It is how you make an image behave like a single command: with ENTRYPOINT ["gunicorn"] set, docker run driftwood/web --workers 4 passes --workers 4 to gunicorn rather than replacing it. The image is gunicorn, and the run command tunes it.

CMD — The Default Arguments

CMD provides defaults that docker run can override. With an ENTRYPOINT set, CMD supplies its default arguments; without one, CMD is the whole default command. Either way, anything you type after the image name on docker run replaces it. That is the lever you reach for when you want a sensible default invocation that a caller can still swap out.

The fixed half and the overridable half
ENTRYPOINT
The fixed executable the container always runs. Arguments to docker run become its arguments — overridden only by --entrypoint.
CMD
The default arguments handed to that executable. Anything typed after the image name on docker run replaces it.

The Two Together

ENTRYPOINT fixes what runs, CMD supplies the default arguments, and docker run image <args> swaps the CMD while keeping the ENTRYPOINT. That is the pattern for a tool image with a sensible default invocation: the program is fixed, the arguments have defaults, and a caller overrides only the arguments.

Driftwood — fixed executable plus overridable defaults
ENTRYPOINT ["gunicorn"]
CMD ["--bind", "0.0.0.0:8000", "app:app"]

# docker run driftwood/web                  → gunicorn --bind 0.0.0.0:8000 app:app
# docker run driftwood/web --workers 4 app:app → gunicorn --workers 4 app:app

The first run uses the CMD defaults; the second replaces them while gunicorn stays fixed because it lives in the ENTRYPOINT. Read the two lines together and the contract is plain: gunicorn is the program, and everything after the image name is its arguments.

Exec Form and PID 1

Exec form — ENTRYPOINT ["gunicorn", ...] — makes gunicorn PID 1, so it receives SIGTERM from docker stop and shuts down cleanly. Shell form — ENTRYPOINT gunicorn ... — inserts /bin/sh -c, which becomes PID 1, does not forward signals, and leaves docker stop to time out and SIGKILL after 10 seconds. This is the topic-15 signal lesson made concrete: the form you pick here is the difference between a graceful drain of in-flight requests and a hard kill.

Overriding at Run Time

docker run --entrypoint <other> image replaces the ENTRYPOINT — handy for dropping into a shell to debug — while arguments after the image name replace the CMD. Knowing which lever moves which half is the difference between debugging cleanly and fighting the image: docker run driftwood/web bash hands bash to gunicorn as an argument and fails, but docker run --entrypoint bash driftwood/web drops you into a shell.

CMD vs ENTRYPOINT

ENTRYPOINT — declares the fixed executable the container always runs; docker run arguments become its arguments, not a replacement, and --entrypoint is needed to override it. Use it for the program the image exists to run.

CMD — declares the default command-or-arguments and is replaced by anything you type after the image name on docker run. Use it for the overridable defaults. Write both in exec form ([...]) so the process is PID 1 and receives signals.

Common Mistakes
  • Using shell form ENTRYPOINT gunicorn --bind 0.0.0.0:8000 app:app/bin/sh -c becomes PID 1, gunicorn never sees SIGTERM, and docker stop hangs 10 seconds then SIGKILLs, dropping in-flight requests.
  • Putting the whole command in ENTRYPOINT and leaving CMD empty when you wanted run-time-overridable arguments — now docker run image --workers 4 cannot replace the args, only --entrypoint does, and the image feels rigid.
  • Expecting docker run image bash to drop you into a shell when ENTRYPOINT ["gunicorn"] is set — bash becomes a gunicorn argument, not the command; you need --entrypoint bash.
  • Defining CMD twice (or ENTRYPOINT twice) in one Dockerfile and being surprised only the last one applies — earlier ones are silently ignored.
Best Practices
  • Set ENTRYPOINT to the fixed executable and CMD to its overridable default arguments when the image exists to run one program, so docker run image <args> tunes it cleanly.
  • Write both ENTRYPOINT and CMD in exec form (JSON array) so the process is PID 1 and receives SIGTERM, letting docker stop shut down gracefully.
  • Use --entrypoint to override the executable for debugging (drop to a shell) rather than rewriting the Dockerfile.
  • Reach for an entrypoint.sh script (which exec "$@" as PID 1) when startup needs setup steps, so the real process still ends up as PID 1.
Comparable tools OCI image config stores Entrypoint and Cmd identically, so Podman and containerd honor the same semantics Kubernetes renames them command (= ENTRYPOINT) and args (= CMD) — a frequent confusion at the orchestration boundary Buildpacks set a default process type instead of a hand-written ENTRYPOINT

Knowledge Check

Which of ENTRYPOINT and CMD is overridden by docker run arguments?

  • CMD is replaced by arguments after the image name; ENTRYPOINT stays fixed unless --entrypoint is used
  • ENTRYPOINT is replaced by arguments after the image name; CMD stays fixed unless rebuilt
  • Both of them are replaced together at once by any positional arguments that are passed to a docker run invocation after the image name
  • Neither can be changed at run time; both are frozen into the image config at build time

How do ENTRYPOINT and CMD combine?

  • ENTRYPOINT is the fixed executable and CMD supplies its default, overridable arguments
  • CMD runs first as a setup step, then ENTRYPOINT runs as the main process
  • The daemon runs whichever of the two is listed last in the Dockerfile and silently ignores the other
  • They are concatenated into a single string and passed together to /bin/sh -c at startup

Why does shell-form ENTRYPOINT break signal delivery?

  • It inserts /bin/sh -c as PID 1, which does not forward SIGTERM, so docker stop times out and SIGKILLs
  • Shell form disables signal handling in the kernel for the entire container namespace
  • docker stop sends a different, weaker signal to shell-form containers than it does to exec-form ones
  • Shell form always runs the actual app process in the background, so PID 1 exits almost immediately and the real app is left orphaned

How do you override each at run time when ENTRYPOINT ["gunicorn"] is set?

  • Arguments after the image name replace CMD; --entrypoint replaces the executable
  • docker run image bash drops you into a shell, replacing the ENTRYPOINT directly
  • A --cmd flag replaces the CMD and a --args flag replaces the ENTRYPOINT
  • Neither can be overridden once set; you must rebuild the image with a new ENTRYPOINT

You got correct