Docker 14 🐳 CMD vs ENTRYPOINT: Exec Form vs Shell Form and Override Behaviors
CMD and ENTRYPOINT are the two Dockerfile instructions that determine what a container runs. They are similar enough to be confused and different enough that the confusion causes real problems: containers that ignore their configuration, applications that never receive SIGTERM, and images that cannot be used as a base for anything else. The confusion is compounded by the fact that both instructions have an exec form and a shell form, and the form changes both the process that becomes PID 1 and how arguments are handled.
The rules are precise once they are separated. ENTRYPOINT defines the executable that always runs. CMD defines the default arguments that are appended to the entry point, or, if there is no entry point, the default command itself. When a container starts, Docker combines the two: the ENTRYPOINT is the command, and the CMD is the default arguments, unless the user overrides the CMD by passing arguments to docker run. The exec form runs the executable directly as PID 1, and the shell form runs it under a shell, which changes signal handling and argument passing.
This chapter covers the difference between CMD and ENTRYPOINT, the exec and shell forms of each, how docker run arguments interact with each, the override behaviors, and the patterns for choosing the right instruction and form for a given image.
Key point: ENTRYPOINT defines the executable that always runs; CMD defines the default command or arguments. Passing arguments to docker run replaces the CMD and appends to the ENTRYPOINT. The exec form runs the executable as PID 1 and receives signals; the shell form runs it under /bin/sh -c and does not. Use exec form for signal handling and shell form only when shell features are needed.
Why the distinction matters
The override problem. Users expect to be able to run a different command in a container. An image with a shell and debugging tools should allow docker run my-image bash. If the image uses ENTRYPOINT for everything, the user cannot replace the command without --entrypoint. If it uses CMD, the user’s bash replaces the CMD cleanly.
The argument problem. Some images should accept arguments. A CLI tool packaged as a container should run the tool with whatever arguments the user provides. ENTRYPOINT is the right choice: the tool is fixed, and the arguments are appended. If CMD were used, the user would have to specify the tool every time.
The signal problem. The process that receives SIGTERM is PID 1. The exec form runs the executable as PID 1, so it receives the signal and can shut down cleanly. The shell form runs /bin/sh as PID 1, so the shell receives the signal, and if it does not forward it, the application is killed abruptly after the grace period.
The argument-passing problem. The shell form concatenates the command and its arguments into a string and passes the string to /bin/sh -c. The exec form passes the command and arguments as an array directly to the kernel. The difference matters when arguments contain spaces, quotes, or shell metacharacters: the exec form preserves them exactly, and the shell form interprets them.
The base-image problem. An image built with ENTRYPOINT is harder to reuse as a base for another image because the entry point is fixed. If a downstream image wants to run a different command, it must override the entry point. An image built with only CMD is easier to reuse because the downstream image can supply its own command.
a. CMD alone
When a Dockerfile has CMD and no ENTRYPOINT, the CMD is the command that runs. It is fully overridable: any arguments passed to docker run replace it.
FROM alpine:3.19
CMD ["echo", "hello"]
docker run command | Executed command |
|---|---|
docker run my-image | echo hello |
docker run my-image ls | ls |
docker run my-image echo bye | echo bye |
The user’s command replaces the CMD entirely. This is the pattern for general-purpose images: the CMD is a sensible default, and the user can run anything.
The exec form of CMD runs the executable as PID 1:
CMD ["node", "server.js"]
The shell form runs the command through a shell:
CMD node server.js
The shell form is equivalent to CMD ["/bin/sh", "-c", "node server.js"], which means /bin/sh is PID 1 and node is a child.
b. ENTRYPOINT alone
When a Dockerfile has ENTRYPOINT and no CMD, the entry point is the command that runs. Arguments passed to docker run are appended to it.
FROM alpine:3.19
ENTRYPOINT ["echo", "hello"]
docker run command | Executed command |
|---|---|
docker run my-image | echo hello |
docker run my-image world | echo hello world |
docker run my-image goodbye | echo hello goodbye |
The user’s arguments are appended, not substituted. This is the pattern for images that always run the same executable, such as a CLI tool or a database server.
The exec form of ENTRYPOINT runs the executable as PID 1:
ENTRYPOINT ["node", "server.js"]
The shell form runs the executable under a shell:
ENTRYPOINT node server.js
The shell form is discouraged because it prevents arguments from being appended correctly and because the shell, not the application, is PID 1.
c. CMD and ENTRYPOINT together
The combination is the most common pattern for application images. ENTRYPOINT defines the executable, and CMD provides default arguments.
FROM python:3.12-slim
WORKDIR /app
COPY app.py .
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
docker run command | Executed command |
|---|---|
docker run my-image | python app.py --port 8080 |
docker run my-image --debug | python app.py --debug |
docker run my-image --port 9000 | python app.py --port 9000 |
The CMD is used only when no arguments are passed. When arguments are passed, they replace the CMD and are appended to the ENTRYPOINT. This gives the user control over the arguments while keeping the executable fixed.
This is the pattern for CLI tools packaged as containers. The ENTRYPOINT is the tool, and the CMD is the default subcommand or arguments. Running docker run my-tool runs the tool with defaults, and docker run my-tool --help runs the tool with --help.
d. Override behaviors
The docker run command has three ways to change what the container runs:
| Approach | Effect | Use case |
|---|---|---|
| Arguments after the image name | Replaces CMD, appends to ENTRYPOINT | Normal usage |
--entrypoint flag | Overrides ENTRYPOINT | Debugging or special cases |
--entrypoint with arguments | Replaces both ENTRYPOINT and CMD | Full control |
The --entrypoint flag takes a value that becomes the new entry point, and any arguments after the image name are passed to it:
docker run --entrypoint /bin/bash my-image
This runs /bin/bash instead of the entry point. With no arguments, the shell starts interactively if the container has a TTY:
docker run -it --entrypoint /bin/bash my-image
Combining --entrypoint with arguments passes the arguments to the new entry point:
docker run --entrypoint /bin/ls my-image -la /
This runs /bin/ls -la /, overriding both the entry point and the CMD.
The --entrypoint flag is a debugging tool. In normal use, the image’s entry point and command should be designed to accept arguments without requiring the flag.
e. Exec form vs shell form
The exec form and shell form differ in three ways: how the command is executed, what becomes PID 1, and how arguments are handled.
| Aspect | Exec form | Shell form |
|---|---|---|
| Syntax | CMD ["exe", "arg"] | CMD exe arg |
| Execution | Directly by the kernel | Under /bin/sh -c |
| PID 1 | The executable | /bin/sh |
| Signal handling | Executable receives signals | Shell receives signals |
| Variable expansion | None | Shell expands $VAR |
| Argument preservation | Exact array | Shell interprets |
The exec form is the recommended default. It gives the application control over signal handling and preserves arguments exactly. The shell form is used only when shell features are needed: variable expansion, piping, redirection, or command chaining.
# Exec form: node is PID 1
CMD ["node", "server.js"]
# Shell form: shell expands $PORT, then runs node
CMD node --port $PORT server.js
When the shell form is used and signal handling matters, the exec built-in replaces the shell with the command:
CMD exec node server.js
This runs node server.js with the shell’s exec, so node becomes PID 1 and receives signals. This is the workaround for cases where shell features are needed before the application starts but the application must still be PID 1.
A common pattern is a shell script that sets up environment variables and then execs the application:
#!/bin/sh
export CONFIG_PATH=/etc/app/config.json
exec node server.js
The script runs, sets up the environment, and exec replaces it with node. The container has one process, and node is PID 1.
f. Choosing the right combination
The choice of instruction and form depends on what the image is for.
| Image type | Recommendation | Reason |
|---|---|---|
| Application with fixed executable | ENTRYPOINT (exec) + CMD (args) | Executable is fixed, args are flexible |
| General-purpose image | CMD (exec) only | User can run any command |
| CLI tool | ENTRYPOINT (exec) + CMD (default args) | Tool is fixed, args are appended |
| Image needing shell setup | CMD with exec in a script | Shell features plus PID 1 |
| Database or service | ENTRYPOINT (exec) or CMD (exec) | Signals must reach the service |
For an application with a fixed executable, ENTRYPOINT and CMD together give the best behavior: the executable is always the same, and the user can override the arguments. For a general-purpose image that should allow any command, CMD alone is right, because it is fully overridable.
For a CLI tool, ENTRYPOINT is right because the tool should always run, and the arguments should be appended. This mirrors how a CLI works: the tool is fixed, and the arguments vary.
For a service, either CMD or ENTRYPOINT works as long as the exec form is used. The important thing is that the service is PID 1 and receives signals.
Complete Example Session
# ============================================
# PART 1: CMD ONLY — OVERRIDABLE
# ============================================
FROM alpine:3.19
CMD ["echo", "hello"]
# docker run my-image → echo hello
# docker run my-image ls → ls
# ============================================
# PART 2: ENTRYPOINT ONLY — FIXED EXECUTABLE
# ============================================
FROM alpine:3.19
ENTRYPOINT ["echo", "hello"]
# docker run my-image → echo hello
# docker run my-image world → echo hello world
# ============================================
# PART 3: ENTRYPOINT AND CMD TOGETHER
# ============================================
FROM python:3.12-slim
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
# docker run my-image → python app.py --port 8080
# docker run my-image --debug → python app.py --debug
# ============================================
# PART 4: EXEC FORM — SIGNALS RECEIVED
# ============================================
FROM node:20-alpine
CMD ["node", "server.js"]
# node is PID 1; SIGTERM is received and handled
# ============================================
# PART 5: SHELL FORM — SHELL IS PID 1
# ============================================
FROM node:20-alpine
CMD node server.js
# /bin/sh is PID 1; SIGTERM goes to the shell
# ============================================
# PART 6: SHELL FORM WITH EXEC
# ============================================
FROM alpine:3.19
RUN apk add --no-cache nginx
CMD exec nginx -g 'daemon off;'
# Shell runs, exec replaces it with nginx
# nginx becomes PID 1
# ============================================
# PART 7: SHELL SCRIPT WITH EXEC
# ============================================
FROM node:20-alpine
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
CMD ["/entrypoint.sh"]
# entrypoint.sh:
# #!/bin/sh
# export NODE_ENV=production
# exec node server.js
# ============================================
# PART 8: OVERRIDE CMD WITH ARGUMENTS
# ============================================
docker run my-image --help
# ENTRYPOINT is kept; --help replaces CMD
# ============================================
# PART 9: OVERRIDE ENTRYPOINT FOR DEBUGGING
# ============================================
docker run -it --entrypoint /bin/bash my-image
# Entry point is replaced by bash
# ============================================
# PART 10: OVERRIDE BOTH
# ============================================
docker run --entrypoint /bin/ls my-image -la /
# Runs: /bin/ls -la /
These ten parts cover CMD only, ENTRYPOINT only, ENTRYPOINT and CMD together, exec form, shell form, shell form with exec, a shell script with exec, overriding CMD with arguments, overriding ENTRYPOINT for debugging, and overriding both.
Quick Reference
Instruction Summary
| Instruction | Purpose | Overridable |
|---|---|---|
CMD | Default command or arguments | Yes, by docker run args |
ENTRYPOINT | Fixed executable | Only by --entrypoint |
Combinations
| Dockerfile | docker run img | docker run img arg |
|---|---|---|
CMD ["a"] | a | arg |
ENTRYPOINT ["a"] | a | a arg |
ENTRYPOINT ["a"] + CMD ["b"] | a b | a arg |
Exec vs Shell Form
| Aspect | Exec | Shell |
|---|---|---|
| Syntax | ["exe", "arg"] | exe arg |
| PID 1 | exe | /bin/sh |
| Signals | Received | Not forwarded |
| Variables | Not expanded | Expanded |
| Arguments | Exact | Interpreted |
Override Flags
| Flag | Effect |
|---|---|
| Arguments after image | Replace CMD, append to ENTRYPOINT |
--entrypoint | Replace ENTRYPOINT |
--entrypoint + args | Replace both |
Best Practices
✅ Do This:
# Use ENTRYPOINT for the executable, CMD for default args
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
# Use exec form for signal handling
CMD ["node", "server.js"]
# Use exec in shell scripts
CMD exec nginx -g 'daemon off;'
# Use CMD only for general-purpose images
CMD ["bash"]
❌ Don’t Do This:
# Shell form when signals matter
CMD node server.js # ❌ /bin/sh is PID 1
# ENTRYPOINT for everything
ENTRYPOINT ["node", "server.js"] # ❌ args replace, not append... wait
# Actually ENTRYPOINT appends; the issue is the user cannot replace the command
# Mixing forms in one instruction
CMD ["echo", "hello"] && echo bye # ❌ invalid syntax
# Multiple CMD
CMD ["echo", "first"] # ❌ ignored
CMD ["echo", "second"] # ❌ only this takes effect
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Cannot run bash in container | ENTRYPOINT is fixed | Use --entrypoint /bin/bash |
| Signals not received | Shell form used | Use exec form or exec |
| Arguments not passed | Shell form ignores appending | Use exec form for ENTRYPOINT |
CMD has no effect | ENTRYPOINT without CMD combination | Understand the interaction |
| Variables not expanded | Exec form used | Use shell form or a script |
| Container exits immediately | No CMD or ENTRYPOINT | Add a command |
Real-World Examples
1. Application with Defaults
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
2. General-Purpose Image
CMD ["bash"]
3. CLI Tool
ENTRYPOINT ["mytool"]
CMD ["--help"]
4. Service with Exec
CMD ["nginx", "-g", "daemon off;"]
5. Shell Script Entry
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
6. Debug Override
docker run -it --entrypoint /bin/sh my-image
7. Pass Arguments
docker run my-tool --verbose --output /tmp
8. Override CMD
docker run my-image --port 9000
9. Multiple Processes with Init
docker run --init my-image
10. Healthcheck
HEALTHCHECK CMD curl -f http://localhost/ || exit 1
Visual
CMD vs ENTRYPOINT
┌──────────────────────────────────────────────────────────────┐
│ CMD ONLY: │
│ CMD ["echo", "hello"] │
│ docker run img → echo hello │
│ docker run img ls → ls │
│ │
│ ENTRYPOINT ONLY: │
│ ENTRYPOINT ["echo", "hello"] │
│ docker run img → echo hello │
│ docker run img world → echo hello world │
│ │
│ BOTH: │
│ ENTRYPOINT ["python", "app.py"] │
│ CMD ["--port", "8080"] │
│ docker run img → python app.py --port 8080 │
│ docker run img --debug → python app.py --debug │
└──────────────────────────────────────────────────────────────┘
Exec vs Shell Form
┌──────────────────────────────────────────────────────────────┐
│ EXEC FORM: │
│ CMD ["node", "server.js"] │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ PID 1: node │ │
│ │ ← SIGTERM received and handled │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ SHELL FORM: │
│ CMD node server.js │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ PID 1: /bin/sh -c "node server.js" │ │
│ │ ← SIGTERM received (not forwarded) │ │
│ │ └── child: node (does not receive SIGTERM) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Override Behaviors
┌──────────────────────────────────────────────────────────────┐
│ docker run my-image │
│ └── Uses ENTRYPOINT + CMD │
│ │
│ docker run my-image arg │
│ └── Uses ENTRYPOINT + arg (CMD replaced) │
│ │
│ docker run --entrypoint /bin/bash my-image │
│ └── Uses /bin/bash (ENTRYPOINT replaced) │
│ │
│ docker run --entrypoint /bin/ls my-image -la / │
│ └── Uses /bin/ls -la / (both replaced) │
└──────────────────────────────────────────────────────────────┘
Choosing the Right Instruction
┌──────────────────────────────────────────────────────────────┐
│ Is the executable fixed? │
│ │ │
│ ├── Yes ──▶ ENTRYPOINT (exec) + CMD (default args) │
│ │ Example: python app.py + --port 8080 │
│ │ │
│ └── No ──▶ CMD (exec) only │
│ Example: bash │
│ │
│ Do you need shell features before the app starts? │
│ │ │
│ ├── Yes ──▶ Shell script with exec │
│ │ entrypoint.sh: exec node server.js │
│ │ │
│ └── No ──▶ Exec form directly │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
CMD | Default command or arguments |
ENTRYPOINT | Fixed executable |
docker run img | Uses ENTRYPOINT + CMD |
docker run img arg | Uses ENTRYPOINT + arg |
--entrypoint | Replaces ENTRYPOINT |
| Exec form | ["exe", "arg"], executable is PID 1 |
| Shell form | exe arg, /bin/sh is PID 1 |
| Signal handling | Requires exec form |
Shell with exec | CMD exec exe arg for PID 1 |
Multiple CMD | Only the last takes effect |
CMD only | Fully overridable |
ENTRYPOINT only | Arguments appended |
Key takeaways:
CMDis the default command or default arguments;ENTRYPOINTis the fixed executable. With both,ENTRYPOINTis the executable andCMDis the default arguments that the user can override.- Arguments to
docker runreplace theCMDand append to theENTRYPOINT. This is the behavior that makesENTRYPOINTsuitable for CLI tools andCMDsuitable for general-purpose images. - The exec form runs the executable as PID 1 and receives signals. The shell form runs
/bin/shas PID 1 and does not forward signals, which causes unclean shutdowns after the grace period. - The shell form is used only when shell features are needed. Variable expansion, piping, and redirection require a shell. When the shell is used,
execshould replace it with the application so the application becomes PID 1. --entrypointoverrides theENTRYPOINTfor a single run. This is a debugging tool, not a normal usage pattern. It should not be needed for images that are designed correctly.- Only the last
CMDin a Dockerfile takes effect. MultipleCMDinstructions are a mistake; the earlier ones are ignored. - An image with
ENTRYPOINTis harder to reuse as a base. A downstream image that wants a different command must override the entry point. An image withCMDonly is easier to reuse.
Remember: CMD and ENTRYPOINT are the two instructions that define what a container runs, and the distinction between them is about override behavior. CMD is a default that the user can replace; ENTRYPOINT is an executable that the user’s arguments are appended to. When both are present, ENTRYPOINT is the command and CMD is the default arguments. The exec form is the correct choice for signal handling; the shell form is for cases that need shell features, and it should use exec to make the application PID 1. The most common mistakes are using the shell form when signals matter, misunderstanding how arguments interact with ENTRYPOINT, and using ENTRYPOINT when CMD would give the user more control. Understanding these mechanics makes the difference between an image that shuts down cleanly and one that is killed abruptly, between an image that accepts configuration and one that ignores it, and between an image that can be reused as a base and one that cannot.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!