CMD vs ENTRYPOINT & Configuration
CMD vs ENTRYPOINT & Configuration
Every container needs to know what process to run. Docker gives you two instructions to define that — CMD and ENTRYPOINT — and understanding the difference between them is one of the most practically important things you will learn about Dockerfiles. Get it wrong and containers silently ignore arguments, ignore signals, or behave completely differently between development and CI. Get it right and your images work intuitively on the command line, in Kubernetes, and in production.
Startup Semantics: How Docker Picks PID 1
When Docker starts a container it creates a namespaced Linux process. The first process that runs — PID 1 — is the container's init. When PID 1 exits, the container stops. This matters for two reasons:
- Signal propagation: The kernel sends
SIGTERMto PID 1 when you rundocker stop. If PID 1 is a shell wrapper that does not forward signals, your real process never gets a graceful-shutdown signal and Docker will kill it withSIGKILLafter a 10-second timeout — leaving open connections, uncommitted transactions, or half-written files behind. - Zombie reaping: PID 1 is responsible for reaping zombie processes (children that have exited but whose exit status has not been collected). Most application runtimes are not written to do this. Using
exec form(see below) and a minimal init liketiniaddresses both problems.
Shell Form vs Exec Form
Both CMD and ENTRYPOINT accept two syntaxes. This distinction drives everything else.
Shell form wraps your command in /bin/sh -c. That shell becomes PID 1, not your process. It does not forward SIGTERM to children by default, and the shell exits when the command exits — but the shell itself is PID 1, so the timing is unpredictable. Always prefer exec form in production images.
CMD node server.js) will receive SIGTERM at the shell process, not at Node. The shell ignores it (or terminates immediately), and Node gets SIGKILL after Docker's stop timeout. Under Kubernetes, this means your pods will always hit the termination grace period (30 s default) and pods will be force-killed, leaving in-flight HTTP requests unfinished. Switch to exec form: CMD ["node", "server.js"].ENTRYPOINT vs CMD: The Interaction Model
The critical rule is simple: ENTRYPOINT defines the executable; CMD provides default arguments to it. When both are present, Docker concatenates them: ENTRYPOINT + CMD. Arguments passed on the command line replace CMD but never replace ENTRYPOINT (unless you use --entrypoint).
This interaction is what makes CLI-style images possible — images for tools like curl, aws-cli, or custom migration runners where the entrypoint is the tool itself and users pass subcommands as arguments:
ENTRYPOINT when the image has a single, clear purpose (a CLI tool, a server, a worker). Use CMD alone for base images where the caller is expected to provide a completely different command. Use both when you want a fixed executable with configurable default arguments.Environment Variables: Runtime Configuration
The Twelve-Factor App methodology (Factor III) says configuration that varies between deployments must come from environment variables, never baked into the image. Docker gives you two mechanisms:
ENV KEY=value— sets a variable at build time that persists into the running container as a default. Visible indocker inspect.docker run -e KEY=valueor Kubernetesenv:— overrides at runtime. This is the production pattern.
ENV SECRET_KEY=abc123 bakes the secret into every image layer permanently. It appears in docker history, docker inspect, and any image registry you push to. Secrets must arrive at runtime via mounted files (Kubernetes Secrets, Docker secrets), environment variables injected by the orchestrator, or a secrets manager SDK. The image must never carry credential data.Build Arguments: Compile-Time Parameterization
ARG is CMD's build-time equivalent. It defines a variable available only during the docker build phase — it does not persist into the running container. Common uses: pinning dependency versions, toggling debug flags, passing a Git SHA for build provenance.
ARG that changes (like a Git SHA) invalidates the build cache at that layer and all subsequent layers. Place frequently-changing ARG values as late in the Dockerfile as possible — after dependency installation — so your RUN npm ci / RUN pip install layers are still cached. Putting ARG GIT_SHA on line 2 means a full reinstall on every commit.Shell Scripts as Entrypoints: The init Pattern
Complex services often need to do work before the main process starts: wait for a database to be ready, generate a config file from environment variables, run database migrations. The standard pattern is a shell script entrypoint that uses exec to hand off to the main process — preserving PID 1 ownership and signal forwarding.
The exec "$@" at the end of the shell script is the entire trick. Without it, the shell remains PID 1 and your gunicorn process is a child. With it, the shell replaces itself with gunicorn, which becomes PID 1 and receives signals directly. This pattern is used verbatim in the official PostgreSQL, Redis, and Nginx Docker images.
tini as a minimal init system. Add RUN apt-get install -y tini and set ENTRYPOINT ["/usr/bin/tini", "--", "your-app"]. Tini reaps zombies and forwards signals correctly, which bare shells and most application runtimes do not. Kubernetes users can also set shareProcessNamespace: true and let the kubelet handle this.Summary: Decision Guide
- Use exec form for both
CMDandENTRYPOINT— always. - Use
ENTRYPOINTto define what the container is; useCMDfor default arguments the user might override. - Avoid
ENVfor secrets — inject those at runtime from an orchestrator or secrets manager. - Put
ARGinstructions as late in the Dockerfile as possible to preserve the layer cache. - Shell script entrypoints must
exec "$@"as their final line so the main process inherits PID 1.