CMD & ENTRYPOINT
Define how your container starts. Understand exec vs shell form, the difference between CMD and ENTRYPOINT, and how to combine them for flexible, production-ready containers.
CMD & ENTRYPOINT
CMD and ENTRYPOINT both define what runs when a container starts - but they serve different roles and interact in ways that confuse most people at first. Getting this right determines how flexible and well-behaved your container is.
Two Forms: Shell vs Exec
Both instructions support two syntaxes. The choice matters significantly.
Shell form
CMD node server.js
ENTRYPOINT python app.py
Docker wraps the command in /bin/sh -c "...". This means:
- Environment variable expansion works (
$PORT,$HOME) - Your process is NOT PID 1 -
/bin/shis - Shell signals (SIGTERM) aren't forwarded to your process
docker stopmay not work cleanly
Exec form (preferred for CMD and ENTRYPOINT)
CMD ["node", "server.js"]
ENTRYPOINT ["python", "app.py"]
Docker runs the process directly (no shell wrapper). This means:
- Your process IS PID 1 - it receives OS signals directly
docker stopsends SIGTERM directly to your process- Graceful shutdown works correctly
- No variable expansion (use
ENVfor that)
Always use exec form for CMD and ENTRYPOINT in production.
CMD - Overridable Default
CMD sets the default command to run. It can be completely replaced at docker run time:
FROM node:20-alpine
WORKDIR /app
COPY . .
CMD ["node", "server.js"]
# Uses CMD: runs node server.js
docker run myapp
# Overrides CMD: runs node --inspect server.js instead
docker run myapp node --inspect server.js
# Overrides CMD entirely: opens a shell
docker run -it myapp sh
This is CMD's defining characteristic: it's a default that users can replace.
ENTRYPOINT - Fixed Executable
ENTRYPOINT makes the container behave like a specific executable. Arguments passed at docker run are appended to it - they don't replace it:
FROM alpine
ENTRYPOINT ["ping"]
CMD ["localhost"] # Default argument
# Uses CMD as default argument: ping localhost
docker run ping-container
# Appends to ENTRYPOINT: ping google.com
docker run ping-container google.com
# Can't just run 'sh' - it becomes 'ping sh'
docker run ping-container sh # Wrong
To override ENTRYPOINT, you must use --entrypoint:
docker run --entrypoint sh -it ping-container #
CMD + ENTRYPOINT Together
The most powerful pattern - ENTRYPOINT fixes the executable, CMD provides overridable default arguments:
FROM nginx:alpine
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"] # Default flags
# Default: nginx -g "daemon off;"
docker run my-nginx
# Override just the flags
docker run my-nginx -g "daemon off;" -c /custom/nginx.conf
Another real-world example:
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
ENTRYPOINT ["python", "manage.py"]
CMD ["runserver", "0.0.0.0:8000"]
# Start the dev server (uses CMD defaults)
docker run mydjango
# Run migrations instead
docker run mydjango migrate
# Open a shell
docker run mydjango shell
# Run tests
docker run mydjango test
The container is now a manage.py wrapper - everything passed to docker run becomes a subcommand.
CMD vs ENTRYPOINT
You have built an image "echo-test" from a Dockerfile with ENTRYPOINT ["echo"] and CMD ["Hello from CMD"]. You want to first run it with no arguments to see the default message, then run it again to print "Hello from args!" instead. Which sequence of commands is correct?
Only One CMD / ENTRYPOINT Is Used
If you write multiple CMD or ENTRYPOINT instructions, only the last one takes effect:
CMD ["node", "server.js"]
CMD ["node", "worker.js"] # ← Only this one is used
This is often an accidental bug in long Dockerfiles. Keep only one of each.
Signal Handling and PID 1
When your process runs as PID 1 (exec form), it receives signals directly. But PID 1 has special responsibilities - it must handle zombie processes if it spawns children.
For complex applications that fork child processes, use a minimal init system:
FROM node:20-alpine
# Install tini - a minimal init system
RUN apk add --no-cache tini
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
# tini runs as PID 1 and forwards signals correctly
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "cluster.js"]
tini correctly handles SIGTERM forwarding and zombie reaping without adding significant overhead.
Entrypoint Scripts
For containers that need setup before the main process starts (e.g. running migrations, checking environment variables), use an entrypoint script:
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["node", "server.js"]
#!/bin/sh
# entrypoint.sh
set -e
# Run migrations before starting the app
echo "Running database migrations..."
node migrate.js
# Then exec the CMD (replacing this shell process)
exec "$@"
The exec "$@" at the end is critical - it replaces the shell with the CMD process, ensuring it becomes PID 1 and receives signals properly.
Quick Reference
| Scenario | Use |
|---|---|
| Simple app with one start command | CMD ["node", "server.js"] |
| Container as a CLI tool | ENTRYPOINT ["mytool"] + optional CMD ["--help"] |
| App with multiple subcommands | ENTRYPOINT ["python", "manage.py"] + CMD ["runserver"] |
| Pre-start setup (migrations, checks) | Entrypoint shell script + exec "$@" |
| Needs graceful shutdown | Exec form (not shell form) |
Summary
- Exec form (
["node", "server.js"]) is preferred - your process gets PID 1 and receives signals correctly - Shell form (
node server.js) wraps in/bin/sh -c- your process won't receivedocker stopsignals cleanly CMDis a replaceable default - overridden by arguments after the image name indocker runENTRYPOINTis a fixed executable -docker runarguments are appended to it- Combine both:
ENTRYPOINTfor the executable,CMDfor overridable default arguments - Only the last
CMDorENTRYPOINTin a Dockerfile is used