Dockerfile ENTRYPOINT, CMD and `run` Arguments

2023/01/23

Docker uses a combination of 'Endpoint' and 'Command' values to compose the command it uses to launch images. Exactly which values are used depends on which are set and their format.

The documentation gives all the necessary information, but it is spread across a number of entries.

In this post, I'll try to reduce the bewildering list of possible permutations (see below) to a couple of simple choices.

I ran these tests on Docker version 20.10.12 on Ubuntu, but as far as I know, this behaviour is pretty stable.

Running a Docker Image

When Docker launches images, it starts a single process via a single command.

If I run an Alpine Linux image without any extra arguments, this is what happens:

$ docker run -ti alpine:3.17 
/ # 

Docker has run /bin/sh thanks to the CMD instruction in the Alpine Linux Dockerfile.

If we append the ls command this is what we get:

$ docker run -ti alpine:3.17 ls
bin    etc    lib    mnt    proc   run    srv    tmp    var
dev    home   media  opt    root   sbin   sys    usr

Here, ls is the "run argument". It has taken precedence over /bin/sh specified in the Dockerfile.

Knowing exactly what command and arguments get executed depends on a combination of what's in the Dockerfile (and, recursively, the Dockerfiles that it is based on) plus the command line arguments passed to docker run.

By the way, we can also confirm the idea that the command Docker executes is the only process that is started:

$ docker run -ti alpine:latest ps afux
PID   USER     TIME  COMMAND
    1 root      0:00 ps afux

So Docker has run my ps command as PID 1 - the only process running in the container.

Dockerfiles

Dockerfiles can contain the ENTRYPOINT instruction and the CMD instruction.

Only one CMD or ENTRYPOINT should be included per Dockerfile.

If more than one is present, only the last will be used.

Base Image Dockerfiles

If a Dockerfile uses a base image (via FROM), it inherits that image's ENTRYPOINT and CMD.

Most popular base images do not set ENTRYPOINT.

Here's a list:

An exception to this is Redis, which launches a script called docker-entrypoint.sh, which does some setup and then executes whatever was passed to CMD.

So, in the most common case, when we need to override stuff, it's only the CMD instruction that will already have been set, or, if ENTRYPOINT is set, there will be little need to change it.

Formats

ENTRYPOINT and CMD have two formats, shell and JSON.

As we will see below, the format that is used changes the rules about how the final command line is composed.

shell Format

When a simple text is supplied, it is called 'shell' format:

ENTRYPOINT echo Ciao
CMD echo Ciao

JSON Array format

If a well formed JSON array is supplied, it is called the 'exec' form or 'array' form:

ENTRYPOINT ["echo", "Ciao"]
CMD ["echo", "Ciao"]

N.B. Well-formed JSON Array is required - quotes must be ".

If single ' quotes are used, the value is treated as a shell format.

docker run Options and Arguments

--entrypoint

If the --entrypoint parameter is passed to docker run, the value overrides any ENDPOINT instruction that has been specified in a Dockerfile. Note that is also clears any Command that has been supplied.

You can't use the 'JSON Array' format when specifying --entrypoint, the value you pass is always interpreted as being in 'shell' format.

Passing an empty string to --entrypoint is a handy way of nullifying any ENTRYPOINT (and CMD) that may have been set, in order to fall back to using docker run arguments exclusively.

If we don't specify arguments, we get an error:

$ docker run -ti --entrypoint="" busybox:latest
docker: Error response from daemon: No command specified.

Arguments

Any arguments passed to docker run after the image name override any CMD instruction.

How the Entrypoint and Command Interact

Based on the above, from now on, I will only refer to the Entrypoint and Command, however they have been indicated.

If an Entrypoint is supplied in shell format, any Command is ignored.

If, on the other hand, there is a JSON format Entrypoint, any JSON format Command is appended.

But, if the Command is in shell format, /bin/sh -c is prepended to the Command.

I think this last case, of all the combinations available, is the most surprising to the user, and only makes sense in cases where the Entrypoint is a setup script, as in the case of the Redis image above.

Running Directly

When a JSON Entrypoint is supplied, the command line which Docker builds from any Entrypoint and Command is executed directly, not via /bin/sh.

As the final command is not run via a shell, this format does not do variable substitution. This is a point to remember - and often causes confusion. A subtle change, from shell format to JSON format, can cause errors when starting a container.

Running commands directly fails if the first array entry is not in the image's $PATH and is not a full path to an executable.

Running a Shell

When the 'shell' format is used, the executable and arguments obtained from combining any Entrypoint and Command are passed to a command shell, e.g. /bin/sh -c.

Running commands via sh fails if the first array entry is not in the shell's $PATH and is not a full path to an executable.

Permutations of Entrypoint and Command

Below is a list of the permutations of Entrypoint and Command, indicating how Docker interprets them.

'JSON' refers to the 'exec'/'array' JSON format.

Entrypoint typeCommand typeCommand used?Outcome
--nUses base image's `ENTRYPOINT`. If there is none, tries to run '/bin/sh'.
-shellyExecutes Command via `sh`.
-JSONyExecutes Command command directly.
shell-nPasses Entrypoint command to `sh`.
shellshellnPasses Entrypoint command to `sh`, **ignoring** Command.
shellJSONnPasses Entrypoint command to `sh`, **ignoring** Command.
JSON-nExecutes Entrypoint directly.
JSONshellyExecutes Entrypoint, appending '/bin/sh -c', then Command
JSONJSONyExecutes Entrypoint directly, appending Command

What Should I Use?

After trawling through all this, my conclusion is as follows:

  • in general, avoid using an Entrypoint,

  • only use an Entrypoint if you need to do setup before running the container,

  • if necessary, use CMD in your Dockerfiles to override what is set in your base image, and pass arguments to docker run if your own CMD needs to be overridden,

  • use 'shell' format for CMD if you want shell pre-processing (e.g. environment variable substitution), otherwise, use the 'JSON' format,

  • pass --entrypoint="" plus a Command to Docker run if your base image has an ENTRYPOINT instruction that you don't want to run.