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:

Alt Text

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.

Alt Text

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.