ENTRYPOINT vs CMD: Back to Basics
The name "ENTRYPOINT
" always confused me. To me, the name implied that every container must have ENTRYPOINT
defined. But after reading the official documentation, I realized that this is not true!
Fact 1: You need at least one (ENTRYPOINT
or CMD
) defined (in order to run)
If you don't define either, you get an error. Let's try running the alpine image, which has neither ENTRYPOINT
or CMD
defined.
$ docker run alpine
docker: Error response from daemon: No command specified.
See 'docker run --help'.
Fact 2: If just one is defined at runtime, CMD
and ENTRYPOINT
have the same effect
$ cat Dockerfile
FROM alpine
ENTRYPOINT ls /usr
$ docker build -t test .
$ docker run test
bin
lib
local
sbin
share
We get the same results if we use CMD
in place of ENTRYPOINT
.
$ cat Dockerfile
FROM alpine
CMD ls /usr # Using CMD instead
$ docker build -t test .
$ docker run test
bin
lib
local
sbin
share
Even though this example shows no difference between ENTRYPOINT
and CMD
, you can see a difference in the metadata of the containers.
For example- the first Dockerfile (with ENTRYPOINT
defined)
$ docker inspect b52 | jq .[0].Config
{
...
"Cmd": null,
...
"Entrypoint": [
"/bin/sh",
"-c",
"ls /"
],
...
}
Fact 3: For both CMD
and ENTRYPOINT
, there are "shell" and "exec" versions
From the docs:

So far, we have used the "shell" form. This means that our command ls -l
is run inside of /bin/sh -c
. Let's try both forms and inspect the running processes.
"Shell form":
$ cat Dockerfile
FROM alpine
ENTRYPOINT ping www.google.com # "shell" format
$ docker build -t test .
$ docker run -d test
11718250a9a24331fda9a782788ba315322fa879db311e7f8fbbd9905068f701
Then inspect the processes.
$ docker exec 117 ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh -c ping www.google.com
7 root 0:00 ping www.google.com
8 root 0:00 ps
Note that sh -c
is PID 1. Now using the "exec" format:
$ cat Dockerfile
FROM alpine
ENTRYPOINT ["ping", "www.google.com"] # "exec" format
$ docker build -t test .
$ docker run -d test
1398bb37bb533f690402e47f84e43938897cbc69253ed86f0eadb6aee76db20d
$ docker exec 139 ps
PID USER TIME COMMAND
1 root 0:00 ping www.google.com
7 root 0:00 ps
Using the "exec" format, you can see that the ping www.google.com
command is running as PID 1, and there is no sh -c
process. Keep in mind that the above example works exactly the same using CMD
in place of ENTRYPOINT
.
Fact 4: The "exec" form is the recommended form
This is because containers are designed to contain a single process. For instance, signals that are sent to the container are sent to the process running inside the container with PID 1. One interesting way to test this is to run the ping container and try to do ctrl+c to stop the container.
Using the container defined with the "exec" form, the container stops gracefully:
$ cat Dockerfile
FROM alpine
ENTRYPOINT ["ping", "www.google.com"]
$ docker build -t test .
$ docker run test
PING www.google.com (172.217.7.164): 56 data bytes
64 bytes from 172.217.7.164: seq=0 ttl=37 time=0.246 ms
64 bytes from 172.217.7.164: seq=1 ttl=37 time=0.467 ms
^C
--- www.google.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.246/0.344/0.467 ms
$
With the "shell" form, it doesn't work quite as you would expect.
$ cat Dockerfile
FROM alpine
ENTRYPOINT ping www.google.com
$ docker build -t test .
$ docker run test
PING www.google.com (172.217.7.164): 56 data bytes
64 bytes from 172.217.7.164: seq=0 ttl=37 time=0.124 ms
^C^C^C^C64 bytes from 172.217.7.164: seq=4 ttl=37 time=0.334 ms
64 bytes from 172.217.7.164: seq=5 ttl=37 time=0.400 ms
Help, I can't get out! The SIGINT
signal that is sent to the sh
process won’t be forwarded to the subprocess ping
, and the shell won’t exit. If for some reason you really want to use the shell form, a workaround is to use exec
to replace the shell process with the ping
process.
$ cat Dockerfile
FROM alpine
ENTRYPOINT exec ping www.google.com
Fact 5: No Shell? No Environment Variables.
The problem with not running in a shell, is that you don't get the benefits of environment variables (such as $PATH
), and other things that come with using a shell. There are a two problems with the below Dockerfile.
$ cat Dockerfile
FROM openjdk:8-jdk-alpine
WORKDIR /data
COPY *.jar /data
CMD ["java", "-jar", "*.jar"] # "exec" format
The first problem is that since you don't have $PATH
, you need to specify the exact location of the java executable. The second problem is that wildcards are evaluated by the shell, so *.jar
won't resolve properly. After fixing those issues, the resulting Dockerfile is this:
FROM openjdk:8-jdk-alpine
WORKDIR /data
COPY *.jar /data
CMD ["/usr/bin/java", "-jar", "spring.jar"]
Fact 6: CMD
arguments append to end of ENTRYPOINT
... sometimes
This is where it kind of gets confusing. There is a table in the docs that tries to explain it.

I will attempt to explain using words.
Fact 6a If you use "shell" format for ENTRYPOINT
, CMD
is ignored.
$ cat Dockerfile
FROM alpine
ENTRYPOINT ls /usr
CMD blah blah blah blah
$ docker build -t test .
$ docker run test
bin
lib
local
sbin
share
Our blah blah blah blah
was ignored.
FACT 6b If you use "exec" format for ENTRYPOINT
, CMD
arguments are appended after.
$ cat Dockerfile
FROM alpine
ENTRYPOINT ["ls", "/usr"]
CMD ["/var"]
$ docker build -t test .
$ docker run test
/usr:
bin
lib
local
sbin
share
/var:
cache
empty
lib
local
lock
log
opt
run
spool
tmp
The /var
was appended to our ENTRYPOINT
, effectively running ls /usr /var
.
Fact 6c If you use the "exec" format for ENTRYPOINT
, then you need to use the "exec" format for CMD
as well. If you don't, docker tries to add the sh -c
into the arguments that are appended, which could lead to some funky results.
Fact 7: ENTRYPOINT
and CMD
can be overridden via command line flags.
Use the --entrypoint
flag to override ENTRYPOINT
:
docker run --entrypoint [my_entrypoint] test
Anything after the image in the docker run
command overrides CMD
:
docker run test [command 1] [arg1] [arg2]
All of the above facts apply, just keep in mind that developers have the ability to override these flags when they do docker run
. Which leads me to the conclusion...
Enough with the Facts... What Should I do?
Ok, if you've come this far, so here is when you should use ENTRYPOINT
vs CMD
.
I'm going to put this in the perspective of the person creating the Dockerfile. This person is trying to create something for other developers to use.
Use ENTRYPOINT
if you don't want developers to change the executable that is run when the container starts. You can think of your container as an "executable wrapper". A good strategy is to define a "stable" combination of executable + parameters as the ENTRYPOINT
. Then you can (optionally) specify a default CMD
that developers can easily override.
$ cat Dockerfile
FROM alpine
ENTRYPOINT ["ping"]
CMD ["www.google.com"]
$ docker build -t test .
Run with default parameters:
$ docker run test
PING www.google.com (172.217.7.164): 56 data bytes
64 bytes from 172.217.7.164: seq=0 ttl=37 time=0.306 ms
Override CMD
with your own parameters:
$ docker run test www.yahoo.com
PING www.yahoo.com (98.139.183.24): 56 data bytes
64 bytes from 98.139.183.24: seq=0 ttl=37 time=0.590 ms
Use only CMD
(with no ENTRYPOINT
) if you want developers the ability to easily override the executable that is being run. If entrypoint
is defined you can still override the executable using --entrypoint
, but it is a much easier for developers to append the command they want at the end of docker run
.
$ cat Dockerfile
FROM alpine
CMD ["ping", "www.google.com"]
$ docker build -t test .
Ping is nice, but let's start the container with a shell instead.
$ docker run -it test sh
/ # ps
PID USER TIME COMMAND
1 root 0:00 sh
7 root 0:00 ps
/ #
I prefer this method most of the time because it gives developers the flexibility of easily overriding the executable with a shell or some other executable.
Cleanup
If you ran these commands, you have a bunch of stopped containers left on your host. Clean them up:
$ docker system prune
Feedback
Love to hear your thoughts on this article on the comments below. Also, if you know of an easier way to "search" docker inspect
output with jq
so that I can do something like docker inspect [id] | jq *.config
would love to hear about that as well.