My Understanding about RUN, CMD and ENTRYPOINT in Dockerfile

Containerization gives us many benefits from packaging to distribution and others. Thus, Docker is widely used as a containerization tool which is designed for running specific tasks and processes, not for hosting operating systems or like other virtualization tools like VMWare, VirtualBox etc. You create a container to serve a single unit task. Once it completes the given task, it stops. The container life-cycle depends on the ongoing process inside of it. Once the process stops,

the container stops as well.

A Dockerfile defines this process. It is a script made up of instructions on how to build a Docker image. In this script, there are two types of instructions that can define the process running in the container:

  1. ENTRYPOINT
  2. CMD

And for the RUN, it is mainly built for only build time instruction.

Dilemma on ENTRYPOINT and CMD

  • CMD to define default command or executable or parameters of a container.
  • CMD to set command which is possible to override.
  • ENTRYPOINT to set command or executable.
  • ENTRYPOINT is not overridable. (Except –entrypoint flag)
  • Both ENTRYPOINT and CMD works as “last one counts”.

CMD and ENTRYPOINT can have two forms as instructions.

  1. Shell form
  2. Exec form

For better clarity –

CMD echo "Hello World" (shell form)
CMD ["echo", "Hello World"] (exec form)
ENTRYPOINT echo "Hello World" (shell form)
ENTRYPOINT ["echo", "Hello World"] (exec form)

By the way, it is suggested to avoid shell form for side affects or performance issue.

Example of CMD and ENTRYPOINT

FROM ubuntu
RUN echo `date`: I am now printing by RUN statement of Dockerfile

To build this Dockerfile –

docker build -t myimage .

To run the recently created image –

docker run myimage

The output of the above run command will not give anything.

What if I pass command on the docker run <image> command?

docker run myimage ls

It should give me the directory and file list from the root of the container which is supposed to be running.

bin
boot
dev
etc
home
lib
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

How do I know about the “RUN” instructions of the Dockerfile? When I use the docker build command, we will get these –

[+] Building 4.8s (6/6) FINISHED                                                                                                
 => [internal] load build definition from Dockerfile                                                                       0.0s
 => => transferring dockerfile: 131B                                                                                       0.0s
 => [internal] load .dockerignore                                                                                          0.0s
 => => transferring context: 2B                                                                                            0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                                                           4.6s
 => CACHED [1/2] FROM docker.io/library/ubuntu@sha256:626ffe58f6e7566e00254b638eb7e0f3b11d4da9675088f4781a50ae288f3322     0.0s
 => [2/2] RUN echo `date`: I am now printing by RUN statement of Dockerfile                                     0.2s
 => exporting to image                                                                                                     0.0s
 => => exporting layers                                                                                                    0.0s
 => => writing image sha256:0e83143777f443e74905c6bdd95f554be65eee642a5ec4a7d5cc701bbd030010                               0.0s
 => => naming to docker.io/library/myimage                                                                                 0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

RUN” instruction is not changing anything on the image to check if it executed or not as that is just an echo. To test it, let me modify the Dockerfile –

FROM ubuntu
RUN echo `date`: I am now printing by RUN statement of Dockerfile >> log.txt

If I again build the image using same command, It will create the image fine. But what changed?

docker run myimage

The output will be nothing. But What If I check the folder with “ls”?

docker run myimage ls

Output:

bin
boot
dev
etc
home
lib
log.txt
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

Ok, we have a file “log.txt” inside the image/container.

docker run myimage cat log.txt
Wed Nov 24 15:51:51 UTC 2021: I am now printing by RUN statement of Dockerfile

If I several times run the same command?

% docker run myimage cat log.txt
Wed Nov 24 15:51:51 UTC 2021: I am now printing by RUN statement of Dockerfile
% docker run myimage cat log.txt
Wed Nov 24 15:51:51 UTC 2021: I am now printing by RUN statement of Dockerfile
% docker run myimage cat log.txt
Wed Nov 24 15:51:51 UTC 2021: I am now printing by RUN statement of Dockerfile
% docker run myimage cat log.txt
Wed Nov 24 15:51:51 UTC 2021: I am now printing by RUN statement of Dockerfile
% docker run myimage cat log.txt
Wed Nov 24 15:51:51 UTC 2021: I am now printing by RUN statement of Dockerfile

I am running same image several times. Should be different container.

% docker run myimage hostname       
8ecc0600d24d
% docker run myimage hostname
7b4282538d7c
% docker run myimage hostname
8ea08b4139ba

Different image but giving it same date and time from that log.txt.

So, RUN statement is just once ran when image was built.

Actual Implementations

Now, I will use a basic Python flask application called app.py .

import datetime
import os
import socket
from flask import Flask, jsonify

app = Flask(__name__)


@app.route("/")
def hello_world():
    return jsonify({
        "msg": "Hello there!",
        "hostname": str(socket.gethostname()),
        "date_time": str(datetime.datetime.utcnow())
    })


if __name__ == '__main__':
    app.run(host=os.getenv("host", "0.0.0.0"), port=os.getenv("port", 5115))

To make the image –

FROM ubuntu
RUN apt-get update
RUN apt-get install -y python3 python3-dev python3-pip
ADD myapp /opt/myapp/
WORKDIR /opt/myapp
RUN pip3 install -r requirements.txt
CMD ["python3", "app.py"]

Build the image as usual –

docker build -t myimage .

Run –

% docker run myimage          
 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on all addresses.
   WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://172.17.0.2:5115/ (Press CTRL+C to quit)

What if I run the image with passed parameters?

% docker run myimage ls
app.py
requirements.txt
% docker run myimage pwd
/opt/myapp

So, I can override the CMD.

What If I use ENTRYPOINT?

FROM ubuntu
RUN apt-get update
RUN apt-get install -y python3 python3-dev python3-pip
ADD myapp /opt/myapp/
WORKDIR /opt/myapp
RUN pip3 install -r requirements.txt
ENTRYPOINT ["python3", "app.py"]

What if I run this now?

% docker run myimage   
 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on all addresses.
   WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://172.17.0.2:5115/ (Press CTRL+C to quit)
^C%                                                                                                                             % docker run myimage ls
 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on all addresses.
   WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://172.17.0.2:5115/ (Press CTRL+C to quit)
^C%

So, we can not override the ENTRYPOINT.

Let me try some variations?

FROM ubuntu
RUN apt-get update
RUN apt-get install -y python3 python3-dev python3-pip
ADD myapp /opt/myapp/
WORKDIR /opt/myapp
RUN pip3 install -r requirements.txt
ENTRYPOINT ["python3"]
CMD ["app.py"]
% docker run myimage       
 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on all addresses.
   WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://172.17.0.2:5115/ (Press CTRL+C to quit)
^C%                                                                                                                             % docker run myimage ls
python3: can't open file 'ls': [Errno 2] No such file or directory

So, the above Dockerfile is setting ENTRYPOINT with python3 only and CMD passed to app.py but overriding that can be doable.

Another variation?

FROM ubuntu
RUN apt-get update
RUN apt-get install -y python3 python3-dev python3-pip
ADD myapp /opt/myapp/
WORKDIR /opt/myapp
RUN pip3 install -r requirements.txt
CMD ["app.py"]
ENTRYPOINT ["python3"]

It will act as same. Whatever the order of CMD is, it will always pass after ENTRYPOINT if ENTRYPOINT is used.

I am hoping that I will get these always working after digging these with variations. Thanks all for reading.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.