| |

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 commandExecuted command
docker run my-imageecho hello
docker run my-image lsls
docker run my-image echo byeecho 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 commandExecuted command
docker run my-imageecho hello
docker run my-image worldecho hello world
docker run my-image goodbyeecho 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 commandExecuted command
docker run my-imagepython app.py --port 8080
docker run my-image --debugpython app.py --debug
docker run my-image --port 9000python 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:

ApproachEffectUse case
Arguments after the image nameReplaces CMD, appends to ENTRYPOINTNormal usage
--entrypoint flagOverrides ENTRYPOINTDebugging or special cases
--entrypoint with argumentsReplaces both ENTRYPOINT and CMDFull 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.

AspectExec formShell form
SyntaxCMD ["exe", "arg"]CMD exe arg
ExecutionDirectly by the kernelUnder /bin/sh -c
PID 1The executable/bin/sh
Signal handlingExecutable receives signalsShell receives signals
Variable expansionNoneShell expands $VAR
Argument preservationExact arrayShell 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 typeRecommendationReason
Application with fixed executableENTRYPOINT (exec) + CMD (args)Executable is fixed, args are flexible
General-purpose imageCMD (exec) onlyUser can run any command
CLI toolENTRYPOINT (exec) + CMD (default args)Tool is fixed, args are appended
Image needing shell setupCMD with exec in a scriptShell features plus PID 1
Database or serviceENTRYPOINT (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

InstructionPurposeOverridable
CMDDefault command or argumentsYes, by docker run args
ENTRYPOINTFixed executableOnly by --entrypoint

Combinations

Dockerfiledocker run imgdocker run img arg
CMD ["a"]aarg
ENTRYPOINT ["a"]aa arg
ENTRYPOINT ["a"] + CMD ["b"]a ba arg

Exec vs Shell Form

AspectExecShell
Syntax["exe", "arg"]exe arg
PID 1exe/bin/sh
SignalsReceivedNot forwarded
VariablesNot expandedExpanded
ArgumentsExactInterpreted

Override Flags

FlagEffect
Arguments after imageReplace CMD, append to ENTRYPOINT
--entrypointReplace ENTRYPOINT
--entrypoint + argsReplace 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

PitfallWhy It HappensFix
Cannot run bash in containerENTRYPOINT is fixedUse --entrypoint /bin/bash
Signals not receivedShell form usedUse exec form or exec
Arguments not passedShell form ignores appendingUse exec form for ENTRYPOINT
CMD has no effectENTRYPOINT without CMD combinationUnderstand the interaction
Variables not expandedExec form usedUse shell form or a script
Container exits immediatelyNo CMD or ENTRYPOINTAdd 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

ItemValue
CMDDefault command or arguments
ENTRYPOINTFixed executable
docker run imgUses ENTRYPOINT + CMD
docker run img argUses ENTRYPOINT + arg
--entrypointReplaces ENTRYPOINT
Exec form["exe", "arg"], executable is PID 1
Shell formexe arg, /bin/sh is PID 1
Signal handlingRequires exec form
Shell with execCMD exec exe arg for PID 1
Multiple CMDOnly the last takes effect
CMD onlyFully overridable
ENTRYPOINT onlyArguments appended

Key takeaways:

  • CMD is the default command or default arguments; ENTRYPOINT is the fixed executable. With both, ENTRYPOINT is the executable and CMD is the default arguments that the user can override.
  • Arguments to docker run replace the CMD and append to the ENTRYPOINT. This is the behavior that makes ENTRYPOINT suitable for CLI tools and CMD suitable for general-purpose images.
  • The exec form runs the executable as PID 1 and receives signals. The shell form runs /bin/sh as 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, exec should replace it with the application so the application becomes PID 1.
  • --entrypoint overrides the ENTRYPOINT for 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 CMD in a Dockerfile takes effect. Multiple CMD instructions are a mistake; the earlier ones are ignored.
  • An image with ENTRYPOINT is harder to reuse as a base. A downstream image that wants a different command must override the entry point. An image with CMD only 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!