Skip to main content

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 otherCmd swaps it out entirely.
  • ENTRYPOINT ["prog"] = fixed first part of the command. Anything after docker run img ... becomes its arguments (or CMD does if you do not pass any).
  • The combo ENTRYPOINT + CMD is 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.
  • --entrypoint on docker run overrides ENTRYPOINT itself; CMD is overridden just by trailing args.

Quick example

dockerfile
FROM alpine:3.21 ENTRYPOINT ["echo"] CMD ["hello"]
bash
$ 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/sh

Three 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

dockerfile
FROM nginx:1.27-alpine CMD ["nginx", "-g", "daemon off;"]
bash
$ 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

dockerfile
FROM alpine:3.21 RUN apk add --no-cache curl ENTRYPOINT ["curl"]
bash
$ docker run myimg https://example.com # -> curl https://example.com $ docker run myimg --help # -> curl --help

The image becomes the command. Users pass curl flags directly to docker run.

Pattern 3: ENTRYPOINT + CMD — fixed command with default args

dockerfile
FROM alpine:3.21 RUN apk add --no-cache curl ENTRYPOINT ["curl"] CMD ["--help"]
bash
$ 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)

dockerfile
CMD nginx -g "daemon off;" # shell form

Docker 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

AspectCMD onlyENTRYPOINT onlyENTRYPOINT + CMD
docker run img runsthe CMDthe ENTRYPOINTENTRYPOINT + CMD
docker run img foo bar runsfoo barENTRYPOINT + foo barENTRYPOINT + foo bar
--entrypoint X img foo bar runsX foo barX foo barX foo bar
Best forservices with no argsimage-as-CLICLI with default args

Common mistakes

Using shell form and wondering why docker stop is slow

dockerfile
# 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

dockerfile
# 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.

dockerfile
CMD ["echo", "first"] CMD ["echo", "second"] # Container runs: echo second

No error, no warning. Easy to miss in a long Dockerfile.

Forgetting that docker run img sh replaces CMD, not ENTRYPOINT

dockerfile
ENTRYPOINT ["my-app"] CMD ["--default-flag"]
bash
$ docker run img sh # runs: my-app sh (NOT a shell) $ docker run --entrypoint sh img # actually a shell

With an ENTRYPOINT, you cannot drop into a shell with just trailing args. You need --entrypoint.

Real-world usage

  • Service images (nginx, postgres, redis): CMD only. The default command starts the daemon; advanced users override with docker run img <custom-args> if needed.
  • CLI images (alpine/git, peter-evans/dockerhub-description, aws-cli): ENTRYPOINT set 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 then exec "$@" to run the CMD.
  • CI/CD-friendly images: ENTRYPOINT ["my-tool"] so jobs can run docker 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

dockerfile
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;"]
bash
$ docker run -d -p 8080:80 mywebsite # uses CMD $ docker run -it mywebsite sh # CMD replaced with sh

The image runs nginx by default but stays usable for debugging.

CLI image — ENTRYPOINT + CMD

dockerfile
FROM alpine:3.21 RUN apk add --no-cache curl ENTRYPOINT ["curl"] CMD ["--help"]
bash
$ 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 debugging

The image is curl. The default behavior (showing help) makes the first run informative; passing args gives you the real tool.

Postgres-style entrypoint script

dockerfile
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
bash
$ docker run -e POSTGRES_PASSWORD=dev postgres:16-alpine # entrypoint runs init logic, then exec's postgres which becomes PID 1

The 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Comments

No comments yet