ENTRYPOINT vs CMD: back to basics

Original author: John Zaccone
  • Transfer

Construction


The name ENTRYPOINTalways confused me. This name implies that each container must have a specific instruction ENTRYPOINT. But after reading the official documentation, I realized that this is not true.


Fact 1: You need to define at least one instruction ( ENTRYPOINTor CMD) (to run).


If you do not define any of them, you will receive an error message. Let's try to launch an Alpine Linux image for which neither ENTRYPOINT, nor are defined CMD.


$ docker run alpine
docker: Error response from daemon: No command specified.  
See 'docker run --help'.

Fact 2: If only one of the instructions is defined at run time, then CMDthey ENTRYPOINTwill have the same effect.


$ cat Dockerfile 
FROM alpine  
ENTRYPOINT ls /usr  

$ docker build -t test .

$ docker run test
bin  
lib  
local  
sbin  
share  

We will get the same results if we use CMDinstead ENTRYPOINT.


$ cat Dockerfile 
FROM alpine  
CMD ls /usr  # Using CMD instead  

$ docker build -t test .

$ docker run test
bin  
lib  
local  
sbin  
share  

Although this example shows that there is no difference between ENTRYPOINTand CMD, it can be seen by comparing the metadata of the containers.


For example, the first Dockerfile (with a specific one ENTRYPOINT):


$ docker inspect b52 | jq .[0].Config
{
  ...
  "Cmd": null,
  ...
  "Entrypoint": [
    "/bin/sh",
    "-c",
    "ls /"
  ],
  ...
}

Fact 3: Both for CMDand for ENTRYPOINTthere are shell and exec modes .


From the manual :


ENTRYPOINT has two execution modes:
  • ENTRYPOINT ["executable", "param1", "param2"] (executable form, preferably)
  • ENTRYPOINT command param1 param2 (shell shape)

So far, we have used shell mode , or shell mode . This means that our team ls -lruns inside /bin/sh -c. Let's try both modes and examine the running processes.


Shell mode :


$ cat Dockerfile
FROM alpine  
ENTRYPOINT ping www.google.com  # "shell" format  

$ docker build -t test .

$ docker run -d test
11718250a9a24331fda9a782788ba315322fa879db311e7f8fbbd9905068f701  

Then we will study 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

Notice that the process sh -chas a PID of 1. Now the same thing, using the "exec" mode :


$ 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

We see that when using exec mode, the command ping www.google.comworks with the PID process identifier equal to 1, and the process is sh -cabsent. Keep in mind that the above example works exactly the same if used CMDinstead ENTRYPOINT.


Fact 4: exec mode is recommended.


This is because containers are designed to contain a single process. For example, the signals sent to the container are redirected to the process running inside the container with the PID equal to 1. Very informative experience: to check the redirection, it is useful to start the ping container and try to press ctrl + c to stop the container.


A container defined using exec mode exits successfully:


$ 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  
$

When using shell mode , the container does not work as expected.


$ 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 signal SIGINTthat was directed to the process shwill not be redirected to the subprocess ping, and the shell will not shut down. If for some reason you really want to use shell mode , the way out of this situation is to use a execshell process to replace the process ping.


$ cat Dockerfile
FROM alpine  
ENTRYPOINT exec ping www.google.com  

Fact 5: no shell? There are no environment variables.


The problem of starting NOT in shell mode is that you cannot take advantage of environment variables (such as $PATH) and other features that the shell provides. There are two problems in the Dockerfile below :


$ cat Dockerfile 
FROM openjdk:8-jdk-alpine  
WORKDIR /data  
COPY *.jar /data  
CMD ["java", "-jar", "*.jar"]  # "exec" format  

First problem: since you cannot use the environment variable $PATH, you need to specify the exact location of the java executable. The second problem: wildcard characters are interpreted by the shell itself, so the string *.jarwill not be processed correctly. After fixing these issues, the resulting Dockerfile looks like this:


FROM openjdk:8-jdk-alpine  
WORKDIR /data  
COPY *.jar /data  
CMD ["/usr/bin/java", "-jar", "spring.jar"]  

Fact 6: CMD arguments are appended to the end of the statement ENTRYPOINT... sometimes.


This is where the confusion begins. The manual has a table whose purpose is to clarify this issue.


Entrypointscreen


I'll try to explain on the fingers .


Fact 6a: If you use shell mode for ENTRYPOINT, it is CMDignored.


$ cat Dockerfile 
FROM alpine  
ENTRYPOINT ls /usr  
CMD blah blah blah blah  

$ docker build -t test .

$ docker run test
bin  
lib  
local  
sbin  
share  

The string blah blah blah blahwas ignored.


FACT 6b: When using the exec for ENTRYPOINTthe arguments CMDare added at the end.


$ 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 argument /varwas added to our instructions ENTRYPOINT, which allowed us to efficiently run the command ls/usr/var.


Fact 6c: When using exec mode for an instruction, ENTRYPOINTyou must use exec mode for the instruction as well CMD. If this is not done, Docker will try to add sh -cto the already added arguments, which may lead to some unpredictable results.


Fact 7: Instructions ENTRYPOINTand CMDcan be overridden using command line flags.


A flag --entrypointcan be used to override an instruction ENTRYPOINT:


docker run --entrypoint [my_entrypoint] test  

Everything that follows the name of the image in the command docker runoverrides the instruction CMD:


docker run test [command 1] [arg1] [arg2]  

All of the above facts are true, but keep in mind that developers have the ability to redefine flags in a team docker run. It follows that ...


Enough facts ... What should I do?


Ok, if you read the article to this place, then here is the information in which cases to use ENTRYPOINT, and in which CMD.


This decision I am going to leave to the discretion of the person creating the Dockerfile , which can be used by other developers.


Use ENTRYPOINTif you do not want developers to modify the executable that runs when the container starts. You can imagine that your container is an executable shell . A good strategy would be to define a stable combination of parameters and executable file as ENTRYPOINT. For it, you can (optionally) specify CMDdefault arguments that other developers can override.


$ cat Dockerfile
FROM alpine  
ENTRYPOINT ["ping"]  
CMD ["www.google.com"]  

$ docker build -t test .

Startup with default settings :


$ 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(without definition ENTRYPOINT) if you want developers to easily override the executable. If an entry point is defined, the executable file can still be redefined using the flag --entrypoint. But for developers it will be much more convenient to add the desired command at the end of the line docker run.


$ cat Dockerfile
FROM alpine  
CMD ["ping", "www.google.com"]  

$ docker build -t test .

Ping is good, but let's try to launch a container with a shell instead of a command ping.


$ docker run -it test sh
/ # ps
PID   USER     TIME   COMMAND  
    1 root       0:00 sh
    7 root       0:00 ps
/ # 

I prefer this method for the most part, because it gives developers the freedom to easily override the executable with a shell or other executable.


Cleaning


After running the commands, a bunch of stopped containers remained on the host. Clean them with the following command:


$ docker system prune

Feedback


I will be glad to hear your thoughts on this article in the comments below. In addition, if you know an easier way to search for a docker using jq so that you can do something like docker inspect [id] | jq * .config, write in the comments too.


John Zakkone


Captain Docker and cloud engineer at IBM. Specializes in Agile, microservices, containers, automation, REST, DevOps.


References:


  1. ENTRYPOINT vs CMD: Back to Basics

Also popular now: