Container Entrypoint

Introduction

This time in our series about containers we are going to look a bit more closely into what happens when a container starts and how we can influence that. Specifically we’re going to learn about the ENTRYPOINT and the CMD keywords in a Dockerfile and among other things how they relate to each other.

The Entry Point

Whenever we start a container from an image we need to declare what command(s) shall be executed by the container upon start. This is called the entry point. Usually when we define a Dockerfile we specify such an entry point as (one of the) last items in the declaration. As a sample let’s take a Dockerfile for an overly simple Python application consisting of one single file app.py with code. Usually, when running the Python application directly on our development machine – that is, not in a container – we just start the application using this command

python app.py

consequently our Dockerfile would look like this

FROM python:2.7
RUN mkdir -p /app
WORKDIR /app
COPY . /app
ENTRYPOINT python app.py

note the last line declaring the ENTRYPOINT. The syntax I have chosen in the above sample is one possible way of declaring the entry point. An alternative syntax is using an array of words, i.e.

ENTRYPOINT ["python", "app.py"]

According to the documentation the latter is the preferred way but honestly I find the former much more readable; yet you will see further below why the array syntax makes sense or in certain scenarios is even the only way things are working.

For the moment let’s assume that app.py contains the following code

def add(a, b):
    return a+b;

def mult(a,b):
    return a*b;

if __name__ == "__main__":
    print "+================+"
    print "|  Hello world!  |"
    print "+================+"

We can now build an image using the Dockerfile

docker build -t python-sample .

And we should see something like this

Sending build context to Docker daemon 3.072 kB
Step 1 : FROM python:2.7
2.7: Pulling from library/python

357ea8c3d80b: Pull complete
52befadefd24: Pull complete
3c0732d5313c: Pull complete
ceb711c7e301: Pull complete
4211bb537697: Pull complete
c48f01f3aad7: Pull complete
72d54f32484a: Pull complete
Digest: sha256:675765c16c09a0f1b5031cd8e568b1c33eebd65fd4c31b658033e3793e738d9f
Status: Downloaded newer image for python:2.7
 ---> 1c6554d85da7
Step 2 : RUN mkdir -p /app
 ---> Running in c7ff8af91fb8
 ---> 86e3301def21
Removing intermediate container c7ff8af91fb8
Step 3 : WORKDIR /app
 ---> Running in 8d60e56b76b6
 ---> e47149da1913
Removing intermediate container 8d60e56b76b6
Step 4 : COPY . /app
 ---> 80f74ea1069d
Removing intermediate container 90f933f18c51
Step 5 : ENTRYPOINT python app.py
 ---> Running in e13b53eed6c5
 ---> 5359199efd0c
Removing intermediate container e13b53eed6c5
Successfully built 5359199efd0c

Now we can run a container from our new image like this

docker run -it --rm python-sample

and we should see this output

$ docker run -it --rm python-sample
+================+
|  Hello world!  |
+================+

Overriding the Entry Point

From time to time we want to run a container from an image that has an ENTRYPOINT defined but we want to override this entry point with our own. Luckily the Docker CLI allows us to just do that by using the --entrypoint argument when running a container. As an example: if we want to run a container from our python-sample image but would rather run a bash shell upon start of the container we can use a run command like this

docker run -it --rm --entrypoint /bin/bash python-sample

This possibility to override the entry point cannot be overestimated. It is very very helpful in many scenarios. For me personally the most common use case is to trouble shoot a container by overriding the entry point with a call of a bash shell instead of the predefine entry point. If I am dealing with a simpler image that doesn’t have bash installed like e.g. the Alpine Linux container then I just use the default shell /bin/sh.

Another use case is to run tests. Let’s assume we have a file tests.py in our project which contains some tests.

from app import add, mult

def test_add():
    assert(add(1,1)==2)

def test_mult():
    assert(mult(2,3)==6)
    
if __name__ == "__main__":
    test_add
    test_mult

We can then run a container in test mode as follows

docker run -dt --name test --entrypoint python tests.py python-sample

That is really helpful! We can use one and the same container image to generate ontainers that do slightly or completely different things.

The same also works when we use docker-compose. Assume we have the following docker-compose.yml file in our project

version: '2'
services:
    app:
        build: .
        entrypoint: /bin/bash
        

On line 5 you can see how we declare what entry point to use when starting the container.

The CMD Keyword

Docker also defined a keyword CMD that can be used in a Dockerfile. What we declare as a command is also used during startup of a container. OK, that sounds confusing… Don’t worry, it took my a while too to finally find a good description defining the differences between ENTRYPOINT and CMD.

Let’s illustrate that with a sample. For this sample I am going to use the official Alpine Linux image since it is so small and has everything I need for this demonstration. Let me first start with the following command

docker run --rm -it alpine echo "Hello World"

Here we are running an instance of the alpine image in interactive mode and we tell Docker what command shall be executed upon start of the container. The result as we can see below is as expected, the container just outputs “Hello World” on the terminal. Since we have used --rm in the command the container will be automatically destroyed after its execution has stopped.

Please note that a container stops as soon as the main process running inside the container is terminated. In our simple sample this is of course the case immediately after echoing “Hello World”.

We can now achieve the same with a slightly modified command

docker run --rm -it --entrypoint echo alpine "Hello World"

So, what just happened? We defined the start command using --entrypoint to be the echo command. The part after the image name (alpine) in this case is only the “Hello World” string. It is passed as parameter to the start command. As a result the container is started by Docker with the command echo "Hello World". Now, what’s the difference between the former and the latter? In the former case we did not declare an entry point. In this case Docker automatically assumes /bin/sh -c as start command and passed to it (as string) whatever we define in our docker run command after the image name. In the former case this is echo "Hello World". Thus that container was started with

/bin/sh -c "echo \"Hello World\"".

Now let’s work with a Dockerfile. Our first version of the file looks like this, that is we continue with our Alpine image and the “Hello World” message

FROM alpine
CMD echo "Hello World"

let’s create an image from this

docker build -t my-hello-world .

and run a container of this image

docker run --rm -it my-hello-world

what we should see is

In this sample we did not explicitly define an entry point, thus Docker assumes automatically /bin/sh -c. But we define the parameters with the keyword CMD to be passed to the (implicit) entry point.

According to what I told above we should now also be able to achieve the same result with the following Dockerfile variant

FROM alpine
ENTRYPOINT ["echo"]
CMD ["Hello World"]

and indeed after building the image and running a container we have the same end result

docker build -t my-hello-world-2 .
docker run --rm -it my-hello-world-2

Please note that in the Dockerfile we have to use the array syntax for ENTRYPOINT and CMD to make the sample work.

Now I can override the part defined in the CMD part, e.g. as follows

docker run --rm -it my-hello-world-2 "Hello Gabriel"

and indeed it works as expected

The last variant of my Dockerfile is only using the keyword ENTRYPOINT

FROM alpine
ENTRYPOINT ["echo", "Hello World"]

Again after building the image and running a container we see the expected result

Summary

In this post I have looked a bit deeper into what exactly the ENTRYPOINT and the CMD keywords in a Dockerfile are. It might look like trivial stuff that I have written up here and it certainly is no rocket science. But ofter we overlook the seemingly trivial things and as a consequence come up with solutions that are more complex than necessary or even worse we have a working example and cannot really fully explain”why” it is working.

In the project I am working currently with we have many different containers we deal with and we use them in different environments and context. As a consequence we are heavy users of the variability that ENTRYPOINT and CMD give us.

About Gabriel Schenker

Gabriel N. Schenker started his career as a physicist. Following his passion and interest in stars and the universe he chose to write his Ph.D. thesis in astrophysics. Soon after this he dedicated all his time to his second passion, writing and architecting software. Gabriel has since been working for over 25 years as a consultant, software architect, trainer, and mentor mainly on the .NET platform. He is currently working as senior software architect at Alien Vault in Austin, Texas. Gabriel is passionate about software development and tries to make the life of developers easier by providing guidelines and frameworks to reduce friction in the software development process. Gabriel is married and father of four children and during his spare time likes hiking in the mountains, cooking and reading.
This entry was posted in containers, docker, How To, introduction and tagged , . Bookmark the permalink. Follow any comments here with the RSS feed for this post.

Comments are closed.