×
Community Blog Getting to Know Dockerfile Instructions: Part 3

Getting to Know Dockerfile Instructions: Part 3

This set of tutorials focuses on giving you practical experience on using Dockerfile on Alibaba Cloud.

By Alwyn Botha, Alibaba Cloud Tech Share Author. Tech Share is Alibaba Cloud's incentive program to encourage the sharing of technical knowledge and best practices within the cloud community.

Part 3 of this tutorial set explores :

  1. Dockerfile RUN
  2. Dockerfile CMD
  3. Dockerfile ENTRYPOINT
  4. Dockerfile VOLUME
  5. Dockerfile ARG

These remaining tutorials will not show docker build output since it adds no value most of the time.

To follow the steps in this tutorial, make sure you have access to an Alibaba Cloud Elastic Compute Service instance with a recent version of Docker already installed. You can refer to this tutorial to learn how to install Docker on your Linux server.

Dockerfile RUN

Reference information : https://docs.docker.com/engine/reference/builder/#run

RUN has 2 forms:

RUN (shell form, the command is run in a shell, which by default is /bin/sh -c)

RUN "executable", "param1", "param2"

The RUN instruction will execute its given commands and create the resulting output in a new layer on top of the current image.

RUN is used to create probably the largest part of an image. It runs most of the commands you are used to use at the shell when setting up a server / vps.

Examples :

RUN mkdir ...
RUN useradd ..
RUN apt install ...
RUN copy ...
RUN unzip ...

Let's create a simple Dockerfile to show the exec and shell form in use.

The exec form does not invoke a command shell. This means that normal shell processing does not happen: environment variable substitution does not happen using the exec form.

nano Dockerfile 

Content:

FROM alpine:3.8
RUN echo $HOME
RUN ["echo", "$HOME"]

Run :

docker build --no-cache --tag tutorial:demo --file Dockerfile  .

Expected output :

Sending build context to Docker daemon    258kB
Step 1/3 : FROM alpine:3.8
 ---> 196d12cf6ab1
Step 2/3 : RUN echo $HOME
 ---> Running in f9aed6be9bb4
/root
Removing intermediate container f9aed6be9bb4
 ---> dd939eaa9e43
Step 3/3 : RUN ["echo", "$HOME"]
 ---> Running in 38e244f781bc
$HOME
Removing intermediate container 38e244f781bc
 ---> 3872c058a5bc
Successfully built 3872c058a5bc
Successfully tagged tutorial:demo

shell form - Note at step 2 shell variable substitution does happen.

exec form - Note at step 3 shell variable substitution does not happen.

Dockerfile CMD

https://docs.docker.com/engine/reference/builder/#cmd

The CMD and ENTRYPOINT instructions deserve a long tutorial dedicated to them.

The exercises here is a mere 5 percent basic introduction to those commands.

Your containers exist to provide a software service to others. CMD and ENTRYPOINT are the way you define exactly what software will run during your container startup. The totality of the functionality of the Linux universe is vast. Unfortunately that means CMD and ENTRYPOINT have to be infinitely powerful ( tinkerable ) to allow ANY use case.

I suggest that you visit https://hub.docker.com/explore/ AFTER you finished this set of Dockerfile tutorials. Visit the URL of every one of the top 10 downloaded images. Click on any topmost Dockerfile they have listed. Study how they use CMD and ENTRYPOINT. THIS is what will make you a Dockerfile expert: not all the other easy Dockerfile instructions combined: but the insightful use of CMD and ENTRYPOINT.

For extra expert bonus insights and accelerated learning, download all 10/20 ?? those images and run them on your development server. Also override the CMD and ENTRYPOINTs when you use docker run.

Alibaba has a vast set of cloud services software available at https://www.alibabacloud.com/product?spm=a3c0i.7911826.1160486.82.7d90737b0qnGmg

How you merge, combine and connect those in value adding ways determine your success. CMD and ENTRYPOINT is the way to expose the software in Docker containers.

Here follows my day one ( one-liner Dockerfile ) beginner exercises.

Unfortunately there can only be one CMD instruction in a Dockerfile. So we have to use several tiny Dockerfiles to demo CMD differences.

From https://docs.docker.com/engine/reference/builder/#cmd

The CMD instruction has three forms:

  1. CMD "executable","param1","param2"
  2. CMD "param1","param2"
  3. CMD command param1 param2 (shell form)

from https://docs.docker.com/engine/reference/builder/#cmd

The exec form is parsed as a JSON array, which means that you must use double-quotes (") around words not single-quotes (').

nano Dockerfile

FROM alpine:3.8
CMD ["/bin/echo","hello from CMD - using exec form"]

Run:

docker build --tag tutorial:demo --file Dockerfile  .
docker run --name tutorial tutorial:demo

Expected output

hello from CMD - using exec form

Now CMD using the shell form:

FROM alpine:3.8
CMD /bin/echo "hello from CMD using shell form"

Run:

docker build --tag tutorial:demo --file Dockerfile  .
docker stop -t 0 tutorial ; docker container prune -f;docker ps -a
docker run --name tutorial tutorial:demo

Expected output :

hello from CMD using shell form

The exec form does not invoke a command shell - implication: no shell variable substitution.

If you want shell processing features then use the shell form: first example below:

CMD /bin/echo $HOME will show your $HOME directory

CMD [ "echo", "$HOME" ] will ** not ** show your $HOME directory, but $HOME as is.

For exercise, lets use the shell form to display content of $HOME:

Run :

nano Dockerfile

Enter this text:

FROM alpine:3.8
CMD /bin/echo $HOME 

Run:

docker build --tag tutorial:demo --file Dockerfile  .
docker stop -t 0 tutorial ; docker container prune -f;docker ps -a
docker run --name tutorial tutorial:demo

Expected output:

/root

Its easy to remember: in the Linux shell you use ** /bin/echo $HOME ** to show value of $HOME. It works exactly like that in a Dockerfile using shell syntax of the CMD instruction.

Notice I just use the following to run an image:

docker run --name tutorial tutorial:demo

The CMD instruction defines the command to be executed when running the image. On the docker run I specified no command to execute: CMD defines that echo must run in this case.

The RUN command is used to build an image. It gets executed at build time.

The CMD instruction does not execute anything at build time, but specifies the command to run at docker run time.

You can override the CMD defined in the Dockerfile. You do this by specifying a command to execute when you run the image. For example, run:

docker stop -t 0 tutorial ; docker container prune -f;docker ps -a
docker run -ti -d --name tutorial tutorial:demo /bin/sh -c 'echo ECHO text from run command'

Expected output :

ECHO text from run command

Note that the echo from the CMD in the Dockerfile is not executed. Only the echo text from the docker run command is shown.

Dockerfile ENTRYPOINT

From https://docs.docker.com/engine/reference/builder/#entrypoint

ENTRYPOINT has two forms:

  1. ENTRYPOINT "executable", "param1", "param2"
  2. ENTRYPOINT command param1 param2 ( shell form)

An ENTRYPOINT allows you to configure a container that will run as an executable.

For example, the following will start nginx with its default content, listening on port 80:

docker run -i -t --rm -p 80:80 nginx

Unfortunately the example they gave is incorrect: there is no ENTRYPOINT in the official nginx Dockerfile at https://github.com/nginxinc/docker-nginx/blob/a22b9f46fe3a586b02d974f64441a4c07215dc5d/mainline/stretch/Dockerfile

The last line of the nginx Dockerfile is

CMD ["nginx", "-g", "daemon off;"]

So when you run the previous shown docker run the CMD executes nginx - there is no ENTRYPOINT in that specific Dockerfile.

Confusing.

Let's create our own mini Dockerfile that contains an actual ENTRYPOINT instruction so you can see it in action. Shell form first.

FROM alpine:3.8
ENTRYPOINT /bin/echo ECHO from ENTRYPOINT - shell form

Run:

docker build --tag tutorial:demo --file Dockerfile  .
docker stop -t 0 tutorial ; docker container prune -f;docker ps -a
docker run --rm --name tutorial tutorial:demo

Its getting annoying to stop and prune the container after every run.

Note I used the --rm option above. That removes the exited container automatically.

To see how --rm works do the docker run again:

docker run --rm --name tutorial tutorial:demo

Expected output:

ECHO from ENTRYPOINT - shell form 

again

docker run --rm --name tutorial tutorial:demo

Same output. There is no need to cleanup / prune the exited container. Convenient.

Exec form of ENTRYPOINT example:

nano Dockerfile

FROM alpine:3.8
ENTRYPOINT ["/bin/echo", "ECHO from ENTRYPOINT - exec form"]

Run:

docker build --tag tutorial:demo --file Dockerfile  .
docker run --rm --name tutorial tutorial:demo

Expected output :

ECHO from ENTRYPOINT - exec form

Dockerfile VOLUME

From https://docs.docker.com/engine/reference/builder/#volume

The VOLUME instruction creates a mount point with the specified name and marks it as holding externally mounted volumes from native host or other containers.

Completely right: The VOLUME instruction creates a mount point with the specified name

Confusing: marks it as holding externally mounted volumes from native host or other containers.

It does not hold any external mounted volumes - it creates a new volume internal to the container.

The term volume in Linux is related to the Logical Volume Manager (LVM) - The VOLUME instruction does not mount any such external volumes.

"marks it as holding externally mounted volumes from native host ** or other containers 88." There are NO other containers involved in the VOLUME instruction.

The VOLUME instruction creates a mount point and it creates a directory in the Docker managed /var/lib/docker/volumes/

Preparation: It would help if you have no / few volumes on your development server. So run this command to see your current list of volumes.

docker volume list

The shorter the list the better you will able to see / find the volumes we are going to create. My list is empty so this tutorial will show the full list of volumes that exist at any point.

Let's use the Dockerfile from the official Docker docs and see what happens:

nano Dockerfile

FROM alpine:3.8
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

Run:

docker build --tag tutorial:demo --file Dockerfile  .
docker stop -t 0 tutorial ; docker container prune -f;docker ps -a
docker run --rm -ti -d --name tutorial tutorial:demo /bin/sh -c 'while true; do sleep 60; done'
docker exec -it tutorial /bin/sh

Enter the ls command - see that myvol exists inside the container.

Enter cat myvol/greeting

Expected output :

hello world

exit the container.

The docker run command initializes the newly created volume /myvol with any data that exists at the specified location ( the greeting file ) within the base image.

The name /myvol exists only inside this container. That name is available to no other container.

This Dockerfile results in an image that causes docker run to create a new mount point at /myvol and copy the greeting file into the newly created volume.

myvol is the name and mount point of the volume ONLY inside the container.

No other container can refer to the myvol name / mountpoint.

Important: The volume names generated on your computer will be different. Please use YOUR volume names when following this tutorial.

docker volume list

Expected output :

DRIVER              VOLUME NAME
local               de82d92daf539b7147770877704ed438b053c50744e874e7a6b86655cd50cf44

Run:


docker volume inspect de82d92daf539b7147770877704ed438b053c50744e874e7a6b86655cd50cf44

Expected output :

[
    {
        "CreatedAt": "2018-10-23T15:19:14+02:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/de82d92daf539b7147770877704ed438b053c50744e874e7a6b86655cd50cf44/_data",
        "Name": "de82d92daf539b7147770877704ed438b053c50744e874e7a6b86655cd50cf44",
        "Options": null,
        "Scope": "local"
    }
]

Note the Mountpoint where the data can be found: Run

ls /var/lib/docker/volumes/de82d92daf539b7147770877704ed438b053c50744e874e7a6b86655cd50cf44/_data/

It shows the greeting file.

If we now stop this container ( since it got run with --rm ), Docker also removes the anonymous volume/s associated with the container when the container is removed.

If you do a

docker stop -t 0 tutorial ; docker container prune -f;docker ps -a
docker volume list

you will notice the volume is gone.

More About anon Volumes

Let's run our container with /myvol again.

docker run -ti -d --name tutorial tutorial:demo /bin/sh -c 'while true; do sleep 60; done'

docker volume list should show one anon volume - random volume name

Let's start 3 other Alpine containers using an anon volume /myvol as well.

docker run -d --name alpine1 -v /myvol alpine:3.8  /bin/sh -c 'while true; do sleep 60; done'
docker run -d --name alpine2 -v /myvol alpine:3.8  /bin/sh -c 'while true; do sleep 60; done'
docker run -d --name alpine3 -v /myvol alpine:3.8  /bin/sh -c 'while true; do sleep 60; done'

Run

docker volume list

again.

Expected output : 

 DRIVER              VOLUME NAME
local               0a43eb2703d4a1638e1a466a9cbae5aba1071939a3b047dc7c27d383b83cebea
local               8edd9ff9f14a6038df3013ea7b581aeda41e6045584b6c350ba4b20e60e6f6a2
local               b02d0f7b3c5be418718b1732be0e46c735806b22791dd70fa44036bb2ffa520e
local               f5a2bb741141616255333c54c421926a8a06ac59f5b054e4619a1dfef4da2fbc

Each of our 4 containers have their OWN volume called /myvol - that name is only available to them internally.

Now run

docker stop -t 0 tutorial
docker stop -t 0 alpine1
docker stop -t 0 alpine2
docker stop -t 0 alpine3

We have been using only anon volumes in this tutorial.

Run

docker container prune -f;docker ps -a

shows 4 containers been deleted.

Run

docker volume list

Shows all 4 volumes still exist.

To delete those volumes you need to run:

docker volume prune

answer y at prompt:

Expected output :

WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Volumes:
51742ef4c728f01870e49edcf29ffa0407eee3363c21fd90493a9c786b36f430
0670752370af7bc92e4fbf1ce513dc8b5472169c4d7e8023e6dc207c510a023f
0c797c8a480e12c450870a78a3cc3de3c22e483162882e1972ed399148e3c00e
0e36fa721df75a9bbcf16fbe2aa809845aee016f7f00c2c6bebefbc27b2832c0

Total reclaimed space: 11B

Lessons learnt here:

  1. Anon volumes get created using VOL in a Dockerfile.
  2. Anon volumes can also be created via -volume when running a container.
  3. Anon volumes have no friendly name outside of containers - they are anon.
  4. Anon volumes get deleted automatically if you specify --rm when you run a container.
  5. Use docker volume prune to remove all volumes not used by at least one container.

Named volumes are created outside of containers - and have to be deliberately deleted by name - one by one. Use named volumes for long term data.

Named volumes can be mounted and shared by unlimited number of containers.

Docker volumes is a vast topic - read more at https://docs.docker.com/storage/volumes/

This part of this tutorial only introduced you briefly to anon / anonymous volumes that get created using VOL in a Dockerfile.

There is no way to rename an anon volume - possibly never will be - see https://github.com/moby/moby/issues/31154

If you want nicely named volumes you will need named volumes.

Other containers must use the long anonymous name if they wish to use this volume. Obviously this ugly long name is not ideal so the preferred way is to created a NAMED volume outside the container. Then all containers can use that easy, descriptive volume name.

Dockerfile LABEL

https://docs.docker.com/engine/reference/builder/#label

Images can have an unlimited number of labels. Those labels can be used to describe the image.

You can also use labels to selectively filter only images with a certain label. Let's see all that in action.

nano Dockerfile 

Enter:

FROM alpine:3.8
LABEL version="demo label"
LABEL description="This text illustrates \
that label values can span multiple lines."
CMD echo "container with labels"

Run:

docker build --tag tutorial:demo --file Dockerfile  .

To show all the labels for an image, run

docker inspect tutorial:demo

Extracted snippet output from that command:

            "Labels": {
                "description": "This text illustrates that label values can span multiple lines.",
                "version": "demo label"
            }

Also run:

docker images --filter "label=version=demo label"

Expected output :

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
tutorial            demo                16d6a13eb022        3 minutes ago       4.41MB

Only images with that demo label got shown.

Note that the docker images command does not have a label column.

Even the docker images command with the (--format) option does not have any functionality to print the labels.

https://docs.docker.com/engine/reference/commandline/images/#format-the-output

Very few official images at the official Docker hub https://hub.docker.com use labels.

However you can create vast structures of meta information ( labels ) on all your images. The only other way to select / classify images is via the single TAG it can have.

Dockerfile ARG

From https://docs.docker.com/engine/reference/builder/#arg

The ARG instruction defines a variable that users can pass at build-time to the builder with the docker build command using the --build-arg = flag.

You can include a default value when you define the ARG instruction.

Arguments are normally used to pass things like user names, directory names, software package version numbers, etc to the build.

Let's create a simple Dockerfile and see ARG in use:

nano Dockerfile 

Enter this:

FROM alpine:3.8
ARG target_dir=dir1
WORKDIR /root/${target_dir}
RUN pwd
RUN echo  ${target_dir}
COPY gamefile /root/${target_dir}
COPY anothergame /root/${target_dir}

Run:

docker build --tag tutorial:demo --file Dockerfile  .

Expected output :

Sending build context to Docker daemon    258kB
Step 1/7 : FROM alpine:3.8
 ---> 196d12cf6ab1
Step 2/7 :  ** ARG target_dir=dir1 88
 ---> Running in 15ef296ddf95
Removing intermediate container 15ef296ddf95
 ---> fe6be5de15bb
Step 3/7 : ** WORKDIR /root/${target_dir} 88
 ---> Running in a64dbe343a98
Removing intermediate container a64dbe343a98
 ---> e41509ae6f7b
Step 4/7 : ** RUN pwd 88
 ---> Running in 3b886f0637b4
88 /root/dir1 88

Note how WORKDIR /root/${target_dir} replace the ${target_dir} with dir1 . Step 7 RUN pwd shows ... /root/dir1

Removing intermediate container 3b886f0637b4
 ---> 08468a2fec81
Step 5/7 : ** RUN echo  ${target_dir} 88
 ---> Running in a6a8a8be8252
88  dir1 88
Removing intermediate container a6a8a8be8252
 ---> 48a9ffe90e28
Step 6/7 : COPY gamefile /root/${target_dir}
as ---> a09e827ec1c7
Step 7/7 : COPY anothergame /root/${target_dir}
 ---> d14edc1f440d
Successfully built d14edc1f440d
Successfully tagged tutorial:demo

Step 5 RUN echo ${target_dir} shows dir1 as well.

We can safely assume that step 6 and 7 replace ${target_dir} with dir1 as well.

If you write programs you know how variable substitution works. ARG works exactly the same.

An ARG only exists during build time. It is not an environment variable. PRINTENV at bash shell will not show them - they do not exist in the shell.

This concludes Part 3 of 4: Get to know all the Dockerfile Instructions. Continue reading Part 4 to learn more.

0 0 0
Share on

Alibaba Clouder

2,599 posts | 763 followers

You may also like

Comments