Docker ENTRYPOINT vs CMD: Exec Form, Shell Form, and PID 1 Behavior
ENTRYPOINT sets the executable. CMD provides default arguments. Together they define what runs when a container starts. Using shell form instead of exec form wraps the process in /bin/sh -c, making it a child of sh rather than PID 1 — which breaks signal handling, graceful shutdown, and SIGTERM from Docker stop.
How ENTRYPOINT and CMD combine
The final command Docker executes is the concatenation of ENTRYPOINT + CMD:
| Dockerfile | docker run command | Executed as |
|---|---|---|
| ENTRYPOINT ["/app"] | docker run img | /app |
| ENTRYPOINT ["/app"] + CMD ["--port", "8080"] | docker run img | /app --port 8080 |
| ENTRYPOINT ["/app"] + CMD ["--port", "8080"] | docker run img --port 9090 | /app --port 9090 |
| No ENTRYPOINT + CMD ["node", "server.js"] | docker run img | node server.js |
CMD is the default argument list. Arguments passed to docker run after the image name replace CMD entirely. ENTRYPOINT is harder to override — it requires --entrypoint.
Exec form vs shell form
Both ENTRYPOINT and CMD support two syntaxes with different PID 1 behavior:
# Exec form — recommended
# Process runs directly as PID 1, receives signals
ENTRYPOINT ["node", "server.js"]
# Shell form — wraps in /bin/sh -c
# Actual executed command: /bin/sh -c "node server.js"
# node is a child of sh (PID 2+), not PID 1
ENTRYPOINT node server.js
Shell form breaks signal handling — SIGTERM from docker stop never reaches your application
GotchaDockerWhen ENTRYPOINT or CMD uses shell form, Docker executes /bin/sh -c 'your command'. /bin/sh becomes PID 1 and spawns your application as a child process. When Docker sends SIGTERM (on docker stop), it sends it to PID 1 (/bin/sh). Most shells don't forward signals to child processes — your application never receives SIGTERM, never gracefully shuts down, and Docker kills it with SIGKILL after a 10-second timeout.
Prerequisites
- Linux process signals
- PID 1 behavior
- Docker container lifecycle
Key Points
- Exec form (["node", "server.js"]) runs the process as PID 1 — directly receives SIGTERM.
- Shell form (node server.js) runs /bin/sh as PID 1 — your app is a child and doesn't receive signals.
- docker stop sends SIGTERM to PID 1. If it doesn't exit in 10 seconds, SIGKILL is sent.
- Use exec form for all production containers. Shell form is acceptable for debug/dev Dockerfiles.
Entrypoint scripts for flexible containers
A shell entrypoint script is useful when you need to run initialization before the main process (wait for database, inject secrets, set up config files):
FROM python:3.11-slim
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
COPY . /app
WORKDIR /app
ENTRYPOINT ["/entrypoint.sh"] # exec form
CMD ["python", "server.py"] # default arguments, overridable
#!/bin/sh
set -e
# Run initialization
echo "Waiting for database..."
until nc -z "$DB_HOST" 5432; do
sleep 1
done
# The key pattern: exec replaces the shell process with the application
# This makes the application PID 1 (inheriting signals correctly)
exec "$@"
The exec "$@" at the end replaces the shell with the CMD arguments (e.g., python server.py). Without exec, the shell remains PID 1 and the application is a child — back to the signal handling problem.
# Flexible container: override the default command
docker run myapp python manage.py migrate # runs migration instead
docker run myapp python -m pytest # runs tests instead
# Both cases: entrypoint.sh runs first, then exec "$@" runs the given command
docker exec vs container startup
docker exec bypasses ENTRYPOINT and CMD entirely — it runs a command directly in the running container's namespace:
# ENTRYPOINT and CMD are irrelevant to docker exec
docker exec -it my-container bash
docker exec my-container ls /app
docker exec my-container python manage.py shell
docker exec is useful for debugging a running container without restarting it. The executed command runs as a separate process in the container — it doesn't replace PID 1.
📝Override ENTRYPOINT at runtime
To override ENTRYPOINT (not just CMD), use the --entrypoint flag:
# Override entrypoint to run a shell instead
docker run --entrypoint /bin/bash -it myapp
# Run a one-off command with a different entrypoint
docker run --entrypoint python myapp -c "import sys; print(sys.version)"
# In docker-compose.yml
services:
debug:
image: myapp
entrypoint: /bin/sh
command: ["-c", "while true; do sleep 1; done"]
Overriding the entrypoint is useful for:
- Debugging: drop into a shell when the normal startup fails
- Migrations: run
python manage.py migratewith the same image before starting the server - Tests: run the test suite with the same dependencies as production
A container uses ENTRYPOINT node server.js (shell form). When you run docker stop, the container takes 10 seconds to stop instead of shutting down gracefully. The Node.js server has a SIGTERM handler that should flush connections and exit cleanly. Why doesn't the handler run?
mediumThe SIGTERM handler is implemented correctly in server.js. docker stop sends SIGTERM. The container uses shell form for the ENTRYPOINT directive.
ANode.js doesn't support SIGTERM handlers on Linux
Incorrect.Node.js supports SIGTERM handlers on Linux. process.on('SIGTERM', handler) works correctly when Node.js receives the signal.BShell form wraps the command in /bin/sh -c, making /bin/sh PID 1. Docker sends SIGTERM to PID 1 (/bin/sh), which doesn't forward it to the Node.js child process. Node.js never receives SIGTERM.
Correct!Shell form (ENTRYPOINT node server.js) executes /bin/sh -c 'node server.js'. /bin/sh becomes PID 1; Node.js is a child process. docker stop sends SIGTERM to PID 1 (/bin/sh). sh doesn't forward the signal to its children. Node.js never sees SIGTERM, its handler never runs, and Docker kills the container with SIGKILL after 10 seconds. Fix: use exec form (ENTRYPOINT ["node", "server.js"]) so Node.js runs as PID 1 and receives SIGTERM directly.CSIGTERM handlers require root privileges to register in Docker
Incorrect.Signal handlers don't require root. This isn't the issue.Ddocker stop sends SIGKILL immediately, not SIGTERM
Incorrect.docker stop sends SIGTERM first and waits (default 10 seconds) before sending SIGKILL. The 10-second delay you observe is exactly this timeout — proof that SIGTERM wasn't handled.
Hint:What process is PID 1 when you use shell form, and what does Docker send its stop signal to?