Docker ENTRYPOINT vs CMD — Your Container Lied to You
You combined ENTRYPOINT and CMD in your Dockerfile, the container started the wrong thing, and now you're here. Here's the full breakdown — every combination, the shell vs exec form trap, and the patterns that actually work.
The error happens at 2am. You start your container, and instead of your API server, you get a shell prompt. Or nothing at all. Or your process runs wrapped in a ghost sh that eats SIGTERM like candy — so graceful shutdown takes 10 seconds of Docker waiting before it gives up and sends SIGKILL.
The culprit, nearly every time: you confused ENTRYPOINT and CMD. Or combined them in ways Docker silently accepts — just not how you expected.
CMD: The Default You Can Replace
CMD sets what runs when you start a container — but it’s a suggestion, not a rule. Pass anything after the image name and it gets replaced entirely:
FROM ubuntu
CMD ["echo", "hello from CMD"]
$ docker run myimage
hello from CMD
$ docker run myimage echo goodbye
goodbye
That echo goodbye didn’t append — it replaced. Your entire CMD is gone. This is by design: CMD is the default behavior, not the enforced behavior. Any runtime argument wins.
ENTRYPOINT: The Part That Always Runs
ENTRYPOINT sets the executable that runs no matter what. Runtime arguments don’t replace it — they get passed to it instead:
FROM ubuntu
ENTRYPOINT ["echo"]
CMD ["hello"]
$ docker run myimage
hello
$ docker run myimage goodbye
goodbye
$ docker run --entrypoint cat myimage /etc/hostname
mycontainer-abc123
When both are set, ENTRYPOINT is the executable and CMD becomes its default arguments. Override CMD freely. Override ENTRYPOINT only if you explicitly pass --entrypoint.
Every ENTRYPOINT + CMD Combination, Explained
The Docker docs include this table but don’t dwell on the rows that will wreck your day:
| ENTRYPOINT | CMD | What actually runs |
|---|---|---|
| Not set | Not set | Error — container needs a command from somewhere |
| Not set | ["cmd", "arg"] exec form | cmd arg |
| Not set | cmd arg shell form | /bin/sh -c "cmd arg" |
["entry"] exec form | Not set | entry |
["entry"] exec form | ["arg1", "arg2"] exec form | entry arg1 arg2 ✓ |
["entry"] exec form | cmd arg shell form | entry /bin/sh -c "cmd arg" — almost certainly wrong |
entry shell form | ["arg1"] exec form | /bin/sh -c "entry" — CMD silently ignored |
entry shell form | cmd arg shell form | /bin/sh -c "entry" — CMD silently ignored |
The two rows marked “CMD silently ignored” are responsible for a disproportionate amount of Docker debugging sessions. Shell form ENTRYPOINT does not merge with CMD — it ignores it entirely. Docker won’t warn you about this.
Shell Form vs Exec Form: The Signal Handling Trap
Both instructions accept two forms, and the choice matters more than most Dockerfile tutorials admit.
Exec form (array syntax):
ENTRYPOINT ["nginx", "-g", "daemon off;"]
Your binary runs directly. It becomes PID 1. When Docker sends SIGTERM to stop the container, your process receives it. Graceful shutdown works. Logs flush. Connections close cleanly.
Shell form (plain string):
ENTRYPOINT nginx -g "daemon off;"
Docker runs this as /bin/sh -c "nginx -g daemon off;". The shell is PID 1. When SIGTERM arrives, sh gets it — and sh doesn’t forward signals to child processes. Your container hangs for 10 seconds, receives SIGKILL, and dies without cleanup. Every single time.
Use exec form. Always. For both ENTRYPOINT and CMD.
Three Patterns That Actually Work
Pattern 1: Fixed executable, overridable defaults
The right pattern for most production containers. The binary is fixed; the flags are swappable at runtime:
ENTRYPOINT ["/app/server"]
CMD ["--port", "8080", "--env", "production"]
# Use defaults
docker run myimage
# Override at deploy time
docker run myimage --port 9090 --env staging
Pattern 2: Wrapper script with exec
When you need init logic before your main process (migrations, secret injection, signal trapping), use a wrapper script. The critical line is exec "$@" at the end — it replaces the shell process with your CMD, so your binary becomes PID 1:
#!/bin/sh
set -e
echo "Running migrations..."
/app/migrate
# Hand off to CMD — exec replaces shell, so /app/server becomes PID 1
exec "$@"
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/app/server", "--port", "8080"]
If you skip exec "$@", the shell stays as PID 1 and you’re back to signal handling problems.
Pattern 3: CMD only, no ENTRYPOINT
Fine for development images or tooling containers where you want to run arbitrary commands against the same environment:
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
For production, Pattern 1 or 2 is safer — you don’t want a misconfigured deploy script accidentally running docker run myimage bash and replacing your server with a shell session.
What docker exec Has to Do With This
Nothing. docker exec runs a command in an already-running container. It bypasses ENTRYPOINT entirely. You don’t need to think about ENTRYPOINT at all when you run docker exec mycontainer bash to poke around — you’re talking to the live container environment, not the startup config.
The confusion usually comes from people who debug with docker exec and confirm things work, then wonder why docker run behaves differently. They’re completely separate code paths.
Pre-Ship Checklist
- Both
ENTRYPOINTandCMDuse exec form (array syntax, not plain strings) - If you have a wrapper script, it ends with
exec "$@" - You’ve tested
docker run myimageanddocker run myimage --your-flagto confirm both paths work docker stop mycontainercompletes in under 2 seconds (not 10 — if it’s 10, you have a signal problem)
If you want automated feedback before your Dockerfile goes anywhere near a registry, IO Tools’ Dockerfile Linter catches shell form usage, missing exec in entrypoint scripts, and other patterns that cause silent misbehavior at runtime.
Install Our Extensions
Add IO tools to your favorite browser for instant access and faster searching
恵 Scoreboard Has Arrived!
Scoreboard is a fun way to keep track of your games, all data is stored in your browser. More features are coming soon!
Must-Try Tools
View All New Arrivals
View AllUpdate: Our latest tool was added on May 16, 2026
