Ch. 13 · Docker

Docker Entrypoint and Command Overrides

How ENTRYPOINT and CMD combine into one argv, what docker run arguments replace, and why appending sh to some images never gives you a shell.

~6 min readbeginnerupdated Oct 7, 2026

Every container starts exactly one process. Docker decides what that process is by gluing two image settings together, ENTRYPOINT and CMD, and then letting docker run change part of the result. Once you see it as a single argument list, most of the confusing behaviour (the image that ignores your command, the sh that never opens a shell) stops being confusing.

The examples here were run on Docker Engine with BuildKit against an alpine:3.20 base. The rules haven’t changed in years, so they apply to Engine 27 and 28 just the same.

One argv, built from two instructions

Take this Dockerfile:

FROM alpine:3.20
ENTRYPOINT ["echo", "entrypoint got:"]
CMD ["--port", "8080"]
dockerfile

Docker stores both values in the image config. You can read them back without running anything:

docker inspect --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' myimage
Terminal
Entrypoint=["echo","entrypoint got:"] Cmd=["--port","8080"]
Text

At start-up Docker concatenates them, entrypoint first, and executes the result directly. No shell is involved:

$ docker run --rm myimage
entrypoint got: --port 8080
Text

That’s the whole model. ENTRYPOINT is the fixed part, the executable plus any arguments that should always be there. CMD is the default tail. If an image has only CMD, the tail is the entire command. If it has only ENTRYPOINT, there’s no default tail.

What docker run actually replaces

Anything you type after the image name replaces CMD, all of it, and nothing else:

$ docker run --rm myimage --port 9090
entrypoint got: --port 9090
Text

This is what makes the pattern useful. An image for a CLI tool can set ENTRYPOINT ["mytool"] and CMD ["--help"], and users run docker run mytool lint src/ as if the container were the binary.

It’s also the source of the most common surprise. You want to poke around inside a container, so you append sh:

$ docker run --rm myimage sh
entrypoint got: sh
Text

You didn’t get a shell. sh became an argument to the entrypoint, exactly like --port 9090 did. With a real application image the symptom is usually stranger: the app starts and complains about an unknown flag sh, or it treats sh as a subcommand and exits with a usage message. Interviewers like this one because it tests whether you understand the model or just remember commands.

To change the executable you need --entrypoint, which replaces ENTRYPOINT and also clears the image’s default CMD. Arguments after the image name then become the new tail:

$ docker run --rm --entrypoint sh myimage -c 'echo shell works; whoami'
shell works
root
Text

Note the ordering. --entrypoint takes a single executable, so flags for it go after the image name, not inside the --entrypoint value. Writing --entrypoint "sh -c" makes Docker look for a binary literally named sh -c.

Compose has the same two knobs: entrypoint: and command: in the service definition map onto --entrypoint and the trailing arguments. Kubernetes renames them, which trips people up: a pod’s command overrides ENTRYPOINT and args overrides CMD.

Exec form, shell form and the warning BuildKit prints

Both instructions accept two syntaxes. The JSON array is exec form and runs the program directly. A bare string is shell form, and Docker wraps it in /bin/sh -c. Here’s what that does to an entrypoint:

FROM alpine:3.20
ENTRYPOINT echo "shell entrypoint got: $@"
CMD ["--port", "8080"]
dockerfile

BuildKit flags it during the build:

1 warning found (use docker --debug to expand):
 - JSONArgsRecommended: JSON arguments recommended for ENTRYPOINT to prevent unintended behavior related to OS signals (line 2)
Text

And the stored config shows the wrapper:

Entrypoint=["/bin/sh","-c","echo \"shell entrypoint got: $@\""] Cmd=["--port","8080"]
Text

Run it and the output looks broken:

$ docker run --rm shellimage
shell entrypoint got: 8080
$ docker run --rm shellimage extra
shell entrypoint got:
Text

The CMD values are still appended, but they land after the sh -c script string, where sh treats the first one as $0 (the script name) and only the rest as positional parameters. So --port vanished and extra disappeared entirely. The Dockerfile reference summarises this as shell-form ENTRYPOINT preventing CMD and run arguments from being used, and for practical purposes that’s right: you can’t rely on them.

The signal problem in that warning is the bigger deal in production. With shell form, PID 1 is /bin/sh, and your application is its child. docker stop sends SIGTERM to PID 1. Many minimal shells don’t forward it, so the app never hears it, Docker waits out the 10-second grace period and then sends SIGKILL. You get slow deploys and connections cut mid-request. The note on signal handling covers that in depth.

Interview tip

Shell form is fine for RUN, where you usually want &&, pipes and variable expansion. For ENTRYPOINT and CMD, default to exec form.

Wrapper scripts that hand over properly

Real images often need to do something before the app starts: render a config file from environment variables, wait for a socket, fix permissions on a mounted directory. The standard pattern is a small script as the entrypoint with the real command in CMD:

#!/bin/sh
set -e
echo "preparing config for ${APP_ENV:-dev}"
exec "$@"
Terminal
FROM alpine:3.20
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
CMD ["sleep", "300"]
dockerfile

"$@" expands to the CMD (or your override) with quoting preserved, and exec replaces the shell process with it. Checking the process table in a running container proves the handover worked, since the app is PID 1 and no shell is left behind:

$ docker exec mycontainer ps -o pid,args
PID   COMMAND
    1 sleep 300
    7 ps -o pid,args
Text

Overrides keep working, because the script runs whatever it’s given:

$ docker run --rm wrapped echo hello
preparing config for dev
hello
Text

Forget the exec and the script stays as PID 1 with the app as a child, which brings back the signal problem. Write exec $@ without quotes and arguments containing spaces get split apart. And if the script was saved with Windows line endings, the kernel looks for an interpreter called /bin/sh\r and you get a confusing “no such file or directory” for a file that obviously exists. A script built for the wrong CPU architecture, or a binary entrypoint compiled for one, shows up as an exec format error instead.

Debugging an image you didn’t write

When a container misbehaves at start-up, the order that saves the most time is:

  1. Read the contract first: docker inspect for Entrypoint and Cmd, or docker image history --no-trunc to see the instructions.
  2. Run the default once and read the output and exit code. If it exits straight away, the container exits immediately guide walks through the usual causes.
  3. Get a shell with docker run --rm -it --entrypoint sh image, then run the entrypoint by hand with the CMD values and watch what happens. Distroless images have no shell, so use their :debug variant or attach an ephemeral debug container instead.

Interview practice

An image’s Dockerfile ends with ENTRYPOINT ["python", "-m", "app"] and CMD ["serve", "--workers", "4"]. A colleague runs docker run myapp bash to inspect the filesystem and gets an argparse usage error ending in invalid choice: 'bash'. Explain what happened, give the command they should have used, and say how you’d run a one-off database migration (python -m app migrate) with the same image.

Answer

Docker built the argument list python -m app bash. The trailing bash replaced the whole CMD (serve --workers 4) but the entrypoint stayed, so bash reached the application’s argument parser, which rejected it as an unknown subcommand. Nothing was wrong with the image.

To get a shell, replace the executable: docker run --rm -it --entrypoint bash myapp (or sh if the base image lacks bash). For the migration, keep the entrypoint and replace only the tail: docker run --rm myapp migrate, which produces python -m app migrate. That’s the benefit of putting the fixed part in ENTRYPOINT: operational tasks become short, predictable commands. A strong answer also mentions that --entrypoint clears the default CMD, so the shell starts with no unexpected arguments.

Keep going

More in Docker

read ✓Docker · easy

Docker Build Context and Ignore Rules

What the build context is, why COPY . . can ship your .env and .git, and how to write .dockerignore patterns that match nested folders.

~5 min readread →
read ✓Docker · easy

Docker Layers and Build Cache Invalidation

Why one edited source file can rerun npm install: how BuildKit's layer cache keys each step, and how to order a Dockerfile so rebuilds stay fast.

~5 min readread →
esc