CMD vs ENTRYPOINT in Dockerfile: what is the difference?
CMD and ENTRYPOINT are two Dockerfile instructions that both define what runs when a container starts. The difference shows up the moment a user passes arguments to docker run.
Theory
TL;DR
CMD ["prog", "arg"]= default command, fully replaceable.docker run img otherCmdswaps it out entirely.ENTRYPOINT ["prog"]= fixed first part of the command. Anything afterdocker run img ...becomes its arguments (orCMDdoes if you do not pass any).- The combo
ENTRYPOINT + CMDis the standard pattern for CLI-style images: fixed entrypoint, default args you can override. - Use exec form (
["prog", "arg"], JSON array). Shell form (prog arg) wraps everything in/bin/sh -c, which breaks signal handling. --entrypointondocker runoverridesENTRYPOINTitself;CMDis overridden just by trailing args.
Quick example
FROM alpine:3.21
ENTRYPOINT ["echo"]
CMD ["hello"]$ docker build -t demo .
$ docker run --rm demo
hello # ENTRYPOINT (echo) + CMD (hello)
$ docker run --rm demo bye
bye # ENTRYPOINT (echo) + new CMD (bye)
$ docker run --rm --entrypoint /bin/sh demo -c 'ls /'
bin
lib
usr
# ↑ ENTRYPOINT explicitly overridden; CMD becomes argv to /bin/shThree runs, three behaviors. The ENTRYPOINT + CMD split is what makes that flexibility possible.
The four common patterns
Pattern 1: CMD only — service with no args
FROM nginx:1.27-alpine
CMD ["nginx", "-g", "daemon off;"]$ docker run myimg -> nginx -g "daemon off;"
$ docker run myimg sh -> sh (CMD fully replaced)Use this when the image runs one fixed service and the user never needs to change the command. Most service images.
Pattern 2: ENTRYPOINT only — image is a CLI tool
FROM alpine:3.21
RUN apk add --no-cache curl
ENTRYPOINT ["curl"]$ docker run myimg https://example.com
# -> curl https://example.com
$ docker run myimg --help
# -> curl --helpThe image becomes the command. Users pass curl flags directly to docker run.
Pattern 3: ENTRYPOINT + CMD — fixed command with default args
FROM alpine:3.21
RUN apk add --no-cache curl
ENTRYPOINT ["curl"]
CMD ["--help"]$ docker run myimg -> curl --help (default)
$ docker run myimg https://example.com
# -> curl https://example.com (override)The canonical "image as a CLI with helpful default behavior" pattern.
Pattern 4: shell form (avoid in production)
CMD nginx -g "daemon off;" # shell formDocker silently wraps this as /bin/sh -c 'nginx -g "daemon off;"'. The actual PID 1 in the container is /bin/sh, not nginx. SIGTERM goes to sh, not your app. Result: docker stop waits the full grace period and then SIGKILLs.
Always use exec form (["prog", "arg"]) for production images.
Comparison table
| Aspect | CMD only | ENTRYPOINT only | ENTRYPOINT + CMD |
|---|---|---|---|
docker run img runs | the CMD | the ENTRYPOINT | ENTRYPOINT + CMD |
docker run img foo bar runs | foo bar | ENTRYPOINT + foo bar | ENTRYPOINT + foo bar |
--entrypoint X img foo bar runs | X foo bar | X foo bar | X foo bar |
| Best for | services with no args | image-as-CLI | CLI with default args |
Common mistakes
Using shell form and wondering why docker stop is slow
# WRONG: shell form, sh is PID 1, signals do not reach nginx
CMD nginx -g "daemon off;"
# RIGHT: exec form, nginx is PID 1, SIGTERM reaches it
CMD ["nginx", "-g", "daemon off;"]With shell form, docker stop sends SIGTERM to /bin/sh, which ignores it. After 10 seconds Docker sends SIGKILL. Your app dies hard, no graceful cleanup.
Trying to expand env vars in exec form
# WRONG: exec form does NOT expand $VAR
CMD ["echo", "$HOME"] # prints literal $HOME
# RIGHT 1: shell form (accept the PID 1 trade-off, or use --init)
CMD echo $HOME
# RIGHT 2: invoke a shell explicitly in exec form
CMD ["/bin/sh", "-c", "echo $HOME"]Exec form is execve()-based and does no shell parsing. If you need env-var expansion, you have to call a shell yourself.
Two CMD or two ENTRYPOINT lines
Only the last one wins. Earlier lines are silently ignored.
CMD ["echo", "first"]
CMD ["echo", "second"]
# Container runs: echo secondNo error, no warning. Easy to miss in a long Dockerfile.
Forgetting that docker run img sh replaces CMD, not ENTRYPOINT
ENTRYPOINT ["my-app"]
CMD ["--default-flag"]$ docker run img sh # runs: my-app sh (NOT a shell)
$ docker run --entrypoint sh img # actually a shellWith an ENTRYPOINT, you cannot drop into a shell with just trailing args. You need --entrypoint.
Real-world usage
- Service images (
nginx,postgres,redis):CMDonly. The default command starts the daemon; advanced users override withdocker run img <custom-args>if needed. - CLI images (
alpine/git,peter-evans/dockerhub-description,aws-cli):ENTRYPOINTset to the binary. The image is the CLI. - Hybrid utilities (
postgres:16's entrypoint runs init scripts then execs postgres):ENTRYPOINT ["docker-entrypoint.sh"]+CMD ["postgres"]. The entrypoint script is a wrapper that does setup thenexec "$@"to run the CMD. - CI/CD-friendly images:
ENTRYPOINT ["my-tool"]so jobs can rundocker run myimg <flags>without remembering the binary name.
Follow-up questions
Q: What is the difference between exec form and shell form?
A: Exec form ["prog", "arg"] runs the binary directly via execve() — your program is PID 1, signals reach it, no extra shell process. Shell form prog arg wraps the command in /bin/sh -c '...', so /bin/sh is PID 1. Production images should always use exec form.
Q: Why does my CMD not start the container?
A: CMD only runs when no command is passed to docker run. If you do docker run img bash, the CMD is replaced by bash. Also: CMD does not run if there is also an ENTRYPOINT that ignores its arguments. And only the last CMD in the Dockerfile counts.
Q: What does the docker-entrypoint.sh pattern do?
A: It is a shell script set as ENTRYPOINT. Inside, it does setup (env-var validation, init dirs, run migrations), then exec "$@" to replace itself with the actual app process. The exec makes the app PID 1 (so signals work). Used by postgres, mysql, redis, and many official images.
Q: Can I override both ENTRYPOINT and CMD at once?
A: Yes: docker run --entrypoint /bin/sh myimg -c 'echo hi'. The --entrypoint swaps the entrypoint; everything after the image name becomes the new args (the original CMD).
Q: (Senior) When would you NOT use the ENTRYPOINT + CMD pattern even for a CLI image?
A: When users genuinely need to run unrelated commands inside the same image — e.g., a debug image where you want docker run img sh to just work. With a hard ENTRYPOINT, that needs --entrypoint sh. For tightly-scoped CLIs, the entrypoint is right; for general-purpose images, leaving entrypoint empty (or [""]) keeps docker run img <anything> flexible.
Examples
Service image — CMD only
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY html/ /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]$ docker run -d -p 8080:80 mywebsite # uses CMD
$ docker run -it mywebsite sh # CMD replaced with shThe image runs nginx by default but stays usable for debugging.
CLI image — ENTRYPOINT + CMD
FROM alpine:3.21
RUN apk add --no-cache curl
ENTRYPOINT ["curl"]
CMD ["--help"]$ docker run --rm mycurl -> curl --help
$ docker run --rm mycurl https://api.github.com -> curl https://api.github.com
$ docker run --rm --entrypoint sh mycurl -> shell, for debuggingThe image is curl. The default behavior (showing help) makes the first run informative; passing args gives you the real tool.
Postgres-style entrypoint script
FROM postgres:16-alpine
# Inherits:
# ENTRYPOINT ["docker-entrypoint.sh"]
# CMD ["postgres"]
# The entrypoint script (provided by base image) does:
# - validate POSTGRES_PASSWORD/POSTGRES_DB env vars
# - run initdb if data dir is empty
# - run /docker-entrypoint-initdb.d/* SQL files on first start
# - exec "$@" # becomes: exec postgres$ docker run -e POSTGRES_PASSWORD=dev postgres:16-alpine
# entrypoint runs init logic, then exec's postgres which becomes PID 1The pattern: entrypoint = setup wrapper, CMD = the real command, exec "$@" at the end so the real command becomes PID 1 and gets signals correctly.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.
Comments
No comments yet