Skip to content
Blog

ENTRYPOINT vs CMD: Who Actually Runs in Your Container

ENTRYPOINT sets the executable, CMD sets the default arguments — and the rules for how they combine, override, and break signal handling are the ones every Docker user eventually needs.

Published on • October 13, 2026

AI Assistant

Every Dockerfile tutorial shows CMD ["nginx", "-g", "daemon off;"] and moves on. Then one day you write ENTRYPOINT ["python", "app.py"] and CMD ["--dev"], run docker run myimage --prod, and wonder why the container ignored you — or why docker stop takes ten seconds and then kills your process.

ENTRYPOINT and CMD are the two instructions that decide what a container runs. They interact by a handful of precise rules, and the reason they exist at all becomes obvious once you see how they compose.

The one-sentence version

  • ENTRYPOINT makes the container behave like an executable: it defines the program that always runs.
  • CMD provides the default arguments for that executable, or the default command when there is no entrypoint.

Put them together and the container’s startup command is ENTRYPOINT + CMD. That composition is the entire design.

The interaction matrix

The Dockerfile reference lays out the rules; here they are condensed into what you actually need:

ENTRYPOINTCMDContainer runs
exec form—just the entrypoint
—exec formjust the CMD
exec formexec formentrypoint + CMD as arguments
shell form—/bin/sh -c <entrypoint>
—shell form/bin/sh -c <cmd>

And the override rules at run time:

  • docker run myimage --prod → replaces CMD, appends your args to ENTRYPOINT.
  • docker run --entrypoint /bin/sh myimage → replaces ENTRYPOINT entirely.

So with ENTRYPOINT ["python", "app.py"] and CMD ["--dev"], the container starts python app.py --dev; docker run myimage --prod starts python app.py --prod. This is why entrypoint-plus-CMD is the standard shape for CLI tools packaged as images: the image is the tool, and users pass normal arguments.

Two more mechanical rules that surprise people:

  1. Only the last ENTRYPOINT and last CMD in a Dockerfile take effect. A CMD in a base image is silently overridden by your CMD — and if you define an ENTRYPOINT while inheriting a base CMD, you may be inheriting arguments you never saw. Check both when debugging an inherited image.
  2. The exec form (["a","b"]) bypasses the shell. No |, no $VAR expansion, no signal-wrapping. That is a feature for signals (below) and a footgun for environment variables.

Why exec form matters: signals and PID 1

When you run docker stop, the daemon sends SIGTERM to the container’s PID 1 and waits (10s by default) before SIGKILL. What is PID 1 depends on the form:

# shell form — PID 1 is /bin/sh, your app is its child
CMD python app.py

# exec form — PID 1 IS your app
CMD ["python", "app.py"]

In the shell form, sh forwards nothing; your Python app never sees SIGTERM, doesn’t flush or close connections, and gets hard-killed ten seconds later. Two mitigations:

  • Prefer the exec form — it is the default recommendation in the Dockerfile reference for exactly this reason.
  • If your app genuinely needs a shell feature, use sh -c 'exec python app.py' — exec replaces the shell with your app, restoring correct signal delivery.

This interacts directly with your restart policy (more on those here): an app that ignores SIGTERM also looks like a crash-looping app to an orchestrator doing rolling restarts.

The recipes

Service image (web server, database):

ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

The entrypoint script does setup (env validation, migrations, config templating) then exec "$@", handing off to CMD. exec is mandatory — without it your entrypoint script stays as PID 1.

CLI tool image:

ENTRYPOINT ["docker-migrate"]
CMD ["--help"]

Users run docker run migrate tool --database-url .... Clean CLI ergonomics with zero extra work.

One-shot task:

CMD ["python", "jobs/backfill.py"]

CMD alone is fine when there is nothing to compose — jobs, migrations, build steps.

The debug escape hatch. An image with a hardcoded entrypoint can always be short-circuited:

docker run -it --entrypoint /bin/sh myimage

Worth knowing when a production container is misbehaving and you need a shell now.

Common failure modes

  • “My CMD arguments are ignored.” You have an ENTRYPOINT, and your docker run args replaced CMD — check what the entrypoint actually receives with docker inspect --format '{{.Config.Entrypoint}} {{.Config.Cmd}}' myimage.
  • “The container runs but exits instantly.” ENTRYPOINT + CMD composed into a command that does not exist (ENTRYPOINT ["python"] + inherited base CMD ["nginx"] → python nginx). Inspect both values.
  • “Env vars don’t expand in CMD.” Exec form does not invoke a shell. Either switch to shell form (losing signal handling) or do expansion in an entrypoint script.
  • “Stop takes 10 seconds every time.” Shell-form entrypoint or missing exec — fix PID 1.

ENTRYPOINT, CMD, and multi-stage builds

In multi-stage builds (covered in Docker multi-stage builds), only the final stage’s ENTRYPOINT/CMD matter — but people often copy the builder stage wholesale and inherit a CMD intended for the build step. Pin the runtime command explicitly in the final stage even when it “looks” right from the base image.

Further reading

CMD is a default you expect users to override. ENTRYPOINT is a contract you expect them to keep. Getting that instinct right — executable-ness in ENTRYPOINT, flexibility in CMD, exec form for signals — eliminates an entire category of “why did my container do that” hours.