2. Getting Started with Dockerfiles
Overview
In this chapter, you will study the form and function of a Dockerfile
and its directives, including FROM
, LABEL
, and CMD
, with which you will dockerize an application. The chapter will provide you with knowledge of the layered filesystem of Docker images and the use of caching during the Docker build process. By the end of this chapter, you will be able to write a Dockerfile
using the common directives and build custom Docker images with the Dockerfile
.
Introduction
In the previous chapter, we learned how to run our first Docker container by pulling a pre-built Docker image from the Docker Hub. While it is useful to get pre-built Docker images from Docker Hub, we must know how to create custom Docker images. This is important for running our applications on Docker by installing new packages and customizing the settings of the pre-built Docker images. In this chapter, we are going to learn how to create our custom Docker image and run a Docker container based on it.
This will be done using a text file called a Dockerfile
. This file consists of commands that can be executed by Docker to create a Docker image. Docker images are created from a Dockerfile
using the docker build
(or docker image build
) command.
Note
Beginning with Docker 1.13, the Docker CLI syntax has been restructured to the form of Docker COMMAND SUBCOMMAND. For example, the docker build
command was replaced by the docker image build
command. This restructuring was carried out to clean up the Docker CLI syntax and gain a more consistent grouping of commands. Currently, both syntaxes are supported, but the old syntax is expected to be deprecated in the future.
A Docker image consists of multiple layers, each layer representing the commands provided in the Dockerfile
. These read-only layers are stacked on top of one another to create the final Docker image. Docker images can be stored in a Docker registry, such as Docker Hub, which is a place where you can store and distribute Docker images.
A Docker container is a running instance of the Docker image. One or more Docker containers can be created from a single Docker image using the docker run
(or docker container run
) command. Once a Docker container is created from the Docker image, a new writeable layer will be added on top of the read-only layers from the Docker image. Docker containers can then be listed with the docker ps (or docker container list) command:

Figure 2.1: Image layers and a container layer
As illustrated in the preceding diagram, there can be one or more read-only layers that make up the Docker image. These read-only layers are generated for each command in the Dockerfile
during the Docker image build process. Once a Docker container is created from the image, a new read-write layer (known as the Container layer) will be added on top of the image layers and will host all the changes made on the running container.
In this chapter, we will write our first Dockerfile
, build the Docker image from the Dockerfile
, and run a Docker container from our custom Docker image. Before we can perform any of these tasks, however, we must first define a Dockerfile
.
What Is a Dockerfile?
A Dockerfile
is a text file that contains instructions on how to create a Docker image. These commands are known as directives. A Dockerfile
is a mechanism that we use to create a custom Docker image as per our requirements.
The format of a Dockerfile
is as follows:
# This is a comment DIRECTIVE argument
A Dockerfile
can contain multiple lines of comments and directives. These lines will be executed in order by the Docker Engine while building the Docker image. Like programming languages, a Dockerfile
can also contain comments.
All statements starting with the # symbol will be treated as a comment. Currently, Dockerfiles
only support single-line comments. If you wish you write a multi-line comment, you need to add the # symbol at the beginning of each line.
However, unlike most programming languages, instructions within the Dockerfile
are not case-sensitive. Even though the DIRECTIVE
is case-insensitive, it is a best practice to write all directives in uppercase to distinguish them from arguments.
In the next section, we will discuss the common directives that we can use in Dockerfiles
to create a custom Docker image.
Note
If you are using ubuntu versions later than 18.04, there will be a prompt to enter time zone. Please suppress the prompt with ARG DEBIAN_FRONTEND=non_interactive
Common Directives in Dockerfiles
As discussed in the previous section, a directive is a command that is used to create a Docker image. In this section, we will be discussing the following five Dockerfile
directives:
- The
FROM
directive - The
LABEL
directive - The
RUN
directive - The
CMD
directive - The
ENTRYPOINT
directive
The FROM Directive
A Dockerfile
usually starts with the FROM
directive. This is used to specify the parent image of our custom Docker image. The parent image is the starting point of our custom Docker image. All the customization that we do will be applied on top of the parent image. The parent image can be an image from Docker Hub, such as Ubuntu, CentOS, Nginx, and MySQL. The FROM
directive takes a valid image name and a tag as arguments. If the tag is not specified, the latest
tag will be used.
A FROM directive has the following format:
FROM <image>:<tag>
In the following FROM
directive, we are using the ubuntu
parent image with the 20.04
tag:
FROM ubuntu:20.04
Additionally, we can use the base image if we need to build a Docker image from scratch. The base image, known as the scratch image, is an empty image mostly used to build other parent images.
In the following FROM
directive, we are using the scratch
image to build our custom Docker image from scratch:
FROM scratch
Now, let's understand what a LABEL
directive is in the next section.
The LABEL Directive
A LABEL
is a key-value pair that can be used to add metadata to a Docker image. These labels can be used to organize the Docker images properly. An example would be to add the name of the author of the Dockerfile
or the version of the Dockerfile
.
A LABEL
directive has the following format:
LABEL <key>=<value>
A Dockerfile
can have multiple labels, adhering to the preceding key-value format:
LABEL maintainer=sathsara@mydomain.com LABEL version=1.0 LABEL environment=dev
Or these labels can be included on a single line separated by spaces:
LABEL maintainer=sathsara@mydomain.com version=1.0 environment=dev
Labels on an existing Docker image can be viewed with the docker image inspect
command.
The output should be like the following on running the docker image inspect <image>:<tag>
command:
... ... "Labels": { "environment": "dev", "maintainer": "sathsara@mydomain.com", "version": "1.0" } ... ...
As shown here, the docker image inspect command will output the key-value pairs configured in the Dockerfile
using the LABEL
directive.
In the next section, we will learn how to execute commands during the image build time using the RUN
directive.
The RUN Directive
The RUN
directive is used to execute commands during the image build time. This will create a new layer on top of the existing layer, execute the specified command, and commit the results to the newly created layer. The RUN
directive can be used to install the required packages, update the packages, create users and groups, and so on.
The RUN
directive takes the following format:
RUN <command>
<command>
specifies the shell command you want to execute as part of the image build process. A Dockerfile
can have multiple RUN
directives adhering to the preceding format.
In the following example, we are running two commands on top of the parent image. The apt-get update
is used to update the package repositories, and apt-get install nginx -y
is used to install the Nginx package:
RUN apt-get update RUN apt-get install nginx -y
Alternatively, you can add multiple shell commands to a single RUN
directive by separating them with the &&
symbol. In the following example, we have used the same two commands, but this time in a single RUN
directive, separated by an &&
symbol:
RUN apt-get update && apt-get install nginx -y
Now, let's move on to the next section where we will learn about the CMD
directive.
The CMD Directive
A Docker container is normally expected to run one process. A CMD
directive is used to provide this default initialization command that will be executed when a container is created from the Docker image. A Dockerfile
can execute only one CMD
directive. If there is more than one CMD
directive in the Dockerfile
, Docker will execute only the last one.
The CMD
directive has the following format:
CMD ["executable","param1","param2","param3", ...]
For example, use the following command to echo "Hello World
" as the output of a Docker container:
CMD ["echo","Hello World"]
The preceding CMD
directive will produce the following output when we run the Docker container with the docker container run <image>
command (replace <image>
with the name of the Docker image):
$ docker container run <image> Hello World
However, if we send any command-line arguments with docker container run <image>
, these arguments will take precedence over the CMD
command that we defined. For example, if we execute the following command (replace <image>
with the name of the Docker image), the default "Hello World
" output defined with the CMD
directive will be ignored. Instead, the container will output "Hello Docker !!!
":
$ docker container run <image> echo "Hello Docker !!!"
As we discussed, both the RUN
and CMD
directives can be used to execute a shell command. The main difference between these two directives is that the command provided with the RUN
directive will be executed during the image build process, while the command provided with the CMD
directive will be executed once a container is launched from the built image.
Another notable difference between the RUN
and CMD
directives is that there can be multiple RUN
directives in a Dockerfile
, but there can be only one CMD
directive (if there are multiple CMD
directives, all others except the last one will be ignored).
As an example, we can use the RUN
directive to install a software package during the Docker image build process and the CMD
directive to start the software package once a container is launched from the built image.
In the next section, we will learn about the ENTRYPOINT
directive, which provides the same functionality as the CMD
directive, except for overriding.
The ENTRYPOINT Directive
Similar to the CMD
directive, the ENTRYPOINT
directive is also used to provide this default initialization command that will be executed when a container is created from the Docker image. The difference between the CMD
directive and the ENTRYPOINT
directive is that, unlike the CMD
directive, we cannot override the ENTRYPOINT
command using the command-line parameters sent with the docker container run
command.
Note
The --entrypoint
flag can be sent with the docker container run
command to override the default ENTRYPOINT
of the image.
The ENTRYPOINT
directive has the following format:
ENTRYPOINT ["executable","param1","param2","param3", ...]
Similar to the CMD
directive, the ENTRYPOINT
directive also allows us to provide the default executable and the parameters. We can use the CMD
directive with the ENTRYPOINT
directive to provide additional arguments to the executable.
In the following example, we have used "echo"
as the default command and "Hello"
as the default parameter using the ENTRYPOINT
directive. We have also provided "World"
as the additional parameter using the CMD
directive:
ENTRYPOINT ["echo","Hello"] CMD ["World"]
The output of the echo
command will differ based on how we execute the docker container run
command.
If we launch the Docker image without any command-line parameters, it will output the message as Hello World
:
$ docker container run <image> Hello World
But if we launch the Docker image with additional command-line parameters (for example, Docker
), the output message will be Hello Docker
:
$ docker container run <image> "Docker" Hello Docker
Before discussing the Dockerfile
directives any further, let's start by creating our first Dockerfile
in the next exercise.
Exercise 2.01: Creating Our First Dockerfile
In this exercise, you will create a Docker image that can print the arguments you pass to the Docker image, preceded by the text You are reading
. For example, if you pass hello world
, it will output You are reading hello world
as the output. If no argument is provided, The Docker Workshop
will be used as the standard value:
- Create a new directory named
custom-docker-image
using themkdir
command. This directory will be the context for your Docker image.Context
is the directory that contains all the files needed to successfully build an image:$ mkdir custom-docker-image
- Navigate to the newly created
custom-docker-image
directory using thecd
command as we will be creating all the files required during the build process (including theDockerfile
) within this directory:$ cd custom-docker-image
- Within the
custom-docker-image
directory, create a file namedDockerfile
using thetouch
command:$ touch Dockerfile
- Now, open the
Dockerfile
using your favorite text editor:$ vim Dockerfile
- Add the following content to the
Dockerfile
, save it, and exit from theDockerfile
:# This is my first Docker image FROM ubuntu LABEL maintainer=sathsara@mydomain.com RUN apt-get update CMD ["The Docker Workshop"] ENTRYPOINT ["echo", "You are reading"]
The Docker image will be based on the Ubuntu parent image. You then use the
LABEL
directive to provide the email address of the author of theDockerfile
. The next line executes theapt-get update
command to update the package list of Debian to the latest available version. Finally, you will use theENTRYPOINT
andCMD
directives to define the default executable and parameters of the container.We have provided
echo
as the default executable andYou are reading
as the default parameter that cannot be overridden with command-line parameters. Also, we have providedThe Docker Workshop
as an additional parameter that can be overridden with command-line parameters with adocker container run
command.
In this exercise, we created our first Dockerfile
using the common directives that we learned in the previous sections. The next step of the process is to build the Docker image from the Dockerfile
. You can only run a Docker container after building the Docker image from the Dockerfile
. In the next section, we are going to look at how to build a Docker image from the Dockerfile
.
Building Docker Images
In the last section, we learned how to create a Dockerfile
. The next step of the process is to build a Docker image using the Dockerfile
.
A Docker image is the template used to build Docker containers. This is analogous to how a house plan can be used to create multiple houses from the same design. If you are familiar with object-oriented programming concepts, a Docker image and a Docker container have the same relationship as a class and an object. A class in object-oriented programming can be used to create multiple objects.
A Docker image is a binary file consisting of multiple layers based on the instructions provided in the Dockerfile
. These layers are stacked on top of one another, and each layer is dependent on the previous layer. Each of the layers is a result of the changes from the layer below it. All the layers of the Docker image are read-only. Once we create a Docker container from a Docker image, a new writable layer will be created on top of other read-only layers, which will contain all the modifications made to the container filesystem:

Figure 2.2: Docker image layers
As illustrated in the preceding image, the docker image build command will create a Docker image from the Dockerfile
. The layers of the Docker image will be mapped to the directives provided in the Dockerfile
.
This image build process is initiated by the Docker CLI and executed by the Docker daemon. To generate a Docker image, the Docker daemon needs access to the Dockerfile
, source code (for example, index.html
), and other files (for example, properties files) that are referenced in the Dockerfile
. These files are typically stored in a directory that is known as the build context. This context will be specified while executing the docker image build command. The entire context will be sent to the Docker daemon during the image build process.
The docker image build
command takes the following format:
$ docker image build <context>
We can execute the docker image build command from the folder that contains the Dockerfile
and the other files, as shown in the following example. Note that the dot (.
) at the end of the command is used to denote the current directory:
$ docker image build.
Let's see the Docker image build process for the following sample Dockerfile
:
FROM ubuntu:latest LABEL maintainer=sathsara@mydomain.com CMD ["echo","Hello World"]
This Dockerfile
uses the latest ubuntu
images as the parent image. Then, the LABEL
directive is used to specify sathsara@mydomain.com
as the maintainer. Finally, the CMD
directive is used to echo "Hello World"
as the output of the image.
Once we execute the docker image build command for the preceding Dockerfile
, we can see an output similar to the following on the console during the build process:
Sending build context to Docker daemon 2.048kB Step 1/3 : FROM ubuntu:latest latest: Pulling from library/ubuntu 2746a4a261c9: Pull complete 4c1d20cdee96: Pull complete 0d3160e1d0de: Pull complete c8e37668deea: Pull complete Digest: sha256:250cc6f3f3ffc5cdaa9d8f4946ac79821aafb4d3afc93928 f0de9336eba21aa4 Status: Downloaded newer image for ubuntu:latest ---> 549b9b86cb8d Step 2/3 : LABEL maintainer=sathsara@mydomain.com ---> Running in a4a11e5e7c27 Removing intermediate container a4a11e5e7c27 ---> e3add5272e35 Step 3/3 : CMD ["echo","Hello World"] ---> Running in aad8a56fcdc5 Removing intermediate container aad8a56fcdc5 ---> dc3d4fd77861 Successfully built dc3d4fd77861
The first line of the output is Sending build context to Docker daemon
, which indicates that the building starts by sending the build context to the Docker daemon. All the files available in the context will be sent recursively to the Docker daemon (unless specifically asked to ignore certain files).
Next, there are steps mentioned as Step 1/3
and Step 2/3
, which correspond to the instructions in the Dockerfile
. As the first step, the Docker daemon will download the parent image. In the preceding output shown, Pulling from library/ubuntu indicates this. For each line of the Dockerfile
, a new intermediate container will be created to execute the directive, and once this step is completed, this intermediate container will be removed. The lines Running in a4a11e5e7c27
and Removing intermediate container a4a11e5e7c27
are used to indicate this. Finally, the Successfully built dc3d4fd77861
line is printed when the build is completed without any errors. This line prints the ID of the newly built Docker image.
Now, we can list the available Docker images using the docker image list
command:
$ docker image list
This list contains the locally built Docker images and Docker images pulled from remote Docker repositories:
REPOSITORY TAG IMAGE ID CREATED SIZE <none> <none> dc3d4fd77861 3 minutes ago 64.2MB ubuntu latest 549b9b86cb8d 5 days ago 64.2MB
As shown in the preceding output, we can see two Docker images. The first Docker image with the IMAGE ID of dc3d4fd77861
is the locally built Docker image during the build process. We can see that this IMAGE ID
is identical to the ID in the last line of the docker image build
command. The next image is the ubuntu image that we used as the parent image of our custom image.
Now, let's build the Docker image again using the docker image build
command:
$ docker image build Sending build context to Docker daemon 2.048kB Step 1/3 : FROM ubuntu:latest ---> 549b9b86cb8d Step 2/3 : LABEL maintainer=sathsara@mydomain.com ---> Using cache ---> e3add5272e35 Step 3/3 : CMD ["echo","Hello World"] ---> Using cache ---> dc3d4fd77861 Successfully built dc3d4fd77861
This time, the image build process was instantaneous. The reason for this is the cache. Since we did not change any content of the Dockerfile
, the Docker daemon took advantage of the cache and reused the existing layers from the local image cache to accelerate the build process. We can see that the cache was used this time with the Using cache
lines available in the preceding output.
The Docker daemon will perform a validation step before starting the build process to make sure that the Dockerfile
provided is syntactically correct. In the case of an invalid syntax, the build process will fail with an error message from the Docker daemon:
$ docker image build Sending build context to Docker daemon 2.048kB Error response from daemon: Dockerfile parse error line 5: unknown instruction: INVALID
Now, let's revisit the locally available Docker images with the docker image list
command:
$ docker image list
The command should return the following output:
REPOSITORY TAG IMAGE ID CREATED SIZE <none> <none> dc3d4fd77861 3 minutes ago 64.2MB ubuntu latest 549b9b86cb8d 5 days ago 64.2MB
Note that there was no name for our custom Docker image. This was because we did not specify any repository or tag during the build process. We can tag an existing image with the docker image tag command.
Let's tag our image with IMAGE ID dc3d4fd77861
as my-tagged-image:v1.0
:
$ docker image tag dc3d4fd77861 my-tagged-image:v1.0
Now, if we list our images again, we can see the Docker image name and the tag under the REPOSITORY
and TAG
columns:
REPOSITORY TAG IMAGE ID CREATED SIZE my-tagged-image v1.0 dc3d4fd77861 20 minutes ago 64.2MB ubuntu latest 549b9b86cb8d 5 days ago 64.2MB
We can also tag an image during the build process by specifying the -t
flag:
$ docker image build -t my-tagged-image:v2.0 .
The preceding command will print the following output:
Sending build context to Docker daemon 2.048kB Step 1/3 : FROM ubuntu:latest ---> 549b9b86cb8d Step 2/3 : LABEL maintainer=sathsara@mydomain.com ---> Using cache ---> e3add5272e35 Step 3/3 : CMD ["echo","Hello World"] ---> Using cache ---> dc3d4fd77861 Successfully built dc3d4fd77861 Successfully tagged my-tagged-image:v2.0
This time, in addition to the Successfully built dc3d4fd77861
line, we can see a Successfully tagged my-tagged-image:v2.0
line, which indicates the tagging on our Docker image.
In this section, we learned how to build a Docker image from a Dockerfile
. We discussed the difference between a Dockerfile
and a Docker image. Then, we discussed how a Docker image is made up of multiple layers. We also experienced how caching can accelerate the build process. Finally, we tagged the Docker images.
In the next exercise, we are going to build a Docker image from the Dockerfile
that we created in Exercise 2.01: Creating Our First Dockerfile.
Exercise 2.02: Creating Our First Docker Image
In this exercise, you will build the Docker image from the Dockerfile
that you created in Exercise 2.01: Creating Our First Dockerfile and run a Docker container from the newly built image. First, you will run the Docker image without passing any arguments, expecting You are reading The Docker Workshop as the output. Next, you will run the Docker image with Docker Beginner's Guide
as the argument and expect You are reading Docker Beginner's Guide as the output:
- First, make sure you are in the
custom-docker-image
directory created in Exercise 2.01: Creating Our First Dockerfile. Confirm that the directory contains the followingDockerfile
created in Exercise 2.01: Creating Our First Dockerfile:# This is my first Docker image FROM ubuntu LABEL maintainer=sathsara@mydomain.com RUN apt-get update CMD ["The Docker Workshop"] ENTRYPOINT ["echo", "You are reading"]
- Build the Docker image with the
docker image build
command. This command has the optional-t
flag to specify the tag of the image. Tag your image aswelcome:1.0
:$ docker image build -t welcome:1.0 .
Note
Do not forget the dot (
.
) at the end of the preceding command, which is used to denote the current directory as the build context.It can be seen from the following output that all five steps mentioned in the
Dockerfile
are executed during the build process. The last two lines of the output suggest that the image is successfully built and tagged:Figure 2.3: Building the welcome:1.0 Docker image
- Build this image again without changing the
Dockerfile
content:$ docker image build -t welcome:2.0 .
Note that this build process completed much quicker than the previous process due to the cache being used:
Figure 2.4: Building the welcome:1.0 Docker image using the cache
- Use the
docker image list
command to list all the Docker images available on your computer:$ docker image list
These images are available on your computer, either when you pull them from a Docker registry, or when you build on your computer:
REPOSITORY TAG IMAGE ID CREATED SIZE welcome 1.0 98f571a42e5c 23 minutes ago 91.9MB welcome 2.0 98f571a42e5c 23 minutes ago 91.9MB ubuntu latest 549b9b86cb8d 2 weeks ago 64.2MB
As you can see from the preceding output, there are three Docker images available. The
ubuntu
image is pulled from the Docker Hub, and version (tag
)1.0
and2.0
of thewelcome
images are built on your computer. - Execute the
docker container run
command to start a new container from the Docker image that you built instep 1
(welcome:1.0
):$ docker container run welcome:1.0
The output should be as follows:
You are reading The Docker Workshop
You receive the expected output of
You are reading The Docker Workshop
.You are reading
is due to the parameter provided with theENTRYPOINT
directive, andThe Docker Workshop
comes from the parameter provided with theCMD
directive. - Finally, execute the
docker container run
command again, this time with command-line arguments:$ docker container run welcome:1.0 "Docker Beginner's Guide"
You will get the output
You are reading Docker Beginner's Guide
because of the command-line argument,Docker Beginner's Guide
, and theYou are reading
argument provided in theENTRYPOINT
directive:You are reading Docker Beginner's Guide
In this exercise, we learned how to build a custom Docker image using the Dockerfile
and run a Docker container from the image. In the next section, we are going to learn other Docker directives that we can use in the Dockerfile
.
Other Dockerfile Directives
In the section Common Directives in Dockerfile, we discussed the common directives available for a Dockerfile
. In that section, we discussed FROM
, LABEL
, RUN
, CMD
, and ENTRYPOINT
directives and how to use them to create a simple Dockerfile
.
In this section, we will be discussing more advanced Dockerfile
directives. These directives can be used to create more advanced Docker images. For example, we can use the VOLUME
directive to bind the filesystem of the host machine to a Docker container. This will allow us to persist the data generated and used by the Docker container. Another example is the HEALTHCHECK
directive, which allows us to define health checks to evaluate the health status of Docker containers. We will look into the following directives in this section:
- The
ENV
directive - The
ARG
directive - The
WORKDIR
directive - The
COPY
directive - The
ADD
directive - The
USER
directive - The
VOLUME
directive - The
EXPOSE
directive - The
HEALTHCHECK
directive - The
ONBUILD
directive
The ENV Directive
The ENV directive in Dockerfile
is used to set environment variables. Environment variables are used by applications and processes to get information about the environment in which a process runs. One example would be the PATH
environment variable, which lists the directories to search for executable files.
Environment variables are defined as key-value pairs as per the following format:
ENV <key> <value>
The PATH environment variable is set with the following value:
$PATH:/usr/local/myapp/bin/
Hence, it can be set using the ENV
directive as follows:
ENV PATH $PATH:/usr/local/myapp/bin/
We can set multiple environment variables in the same line separated by spaces. However, in this form, the key
and value
should be separated by the equal to (=
) symbol:
ENV <key>=<value> <key>=<value> ...
In the following example, there are two environment variables configured. The PATH
environment variable is configured with the value of $PATH:/usr/local/myapp/bin/
, and the VERSION
environment variable is configured with the value of 1.0.0
:
ENV PATH=$PATH:/usr/local/myapp/bin/ VERSION=1.0.0
Once an environment variable is set with the ENV
directive in the Dockerfile
, this variable is available in all subsequent Docker image layers. This variable is even available in the Docker containers launched from this Docker image.
In the next section, we will look into the ARG
directive.
The ARG Directive
The ARG
directive is used to define variables that the user can pass at build time. ARG
is the only directive that can precede the FROM
directive in the Dockerfile
.
Users can pass values using --build-arg <varname>=<value>
, as shown here, while building the Docker image:
$ docker image build -t <image>:<tag> --build-arg <varname>=<value> .
The ARG
directive has the following format:
ARG <varname>
There can be multiple ARG
directives in a Dockerfile
, as follows:
ARG USER ARG VERSION
The ARG
directive can also have an optional default value defined. This default value will be used if no value is passed at build time:
ARG USER=TestUser ARG VERSION=1.0.0
Unlike the ENV
variables, ARG
variables are not accessible from the running container. They are only available during the build process.
In the next exercise, we will use the knowledge gained so far to use ENV
and ARG
directives in a Dockerfile
.
Exercise 2.03: Using ENV and ARG Directives in a Dockerfile
Your manager has asked you to create a Dockerfile
that will use ubuntu as the parent image, but you should be able to change the ubuntu version at build time. You will also need to specify the publisher's name and application directory as the environment variables of the Docker image. You will use the ENV
and ARG
directives in the Dockerfile
to perform this exercise:
- Create a new directory named
env-arg-exercise
using themkdir
command:mkdir env-arg-exercise
- Navigate to the newly created
env-arg-exercise
directory using thecd
command:cd env-arg-exercise
- Within the
env-arg-exercise
directory, create a file namedDockerfile
:touch Dockerfile
- Now, open the
Dockerfile
using your favorite text editor:vim Dockerfile
- Add the following content to the
Dockerfile
. Then, save and exit from theDockerfile
:# ENV and ARG example ARG TAG=latest FROM ubuntu:$TAG LABEL maintainer=sathsara@mydomain.com ENV PUBLISHER=packt APP_DIR=/usr/local/app/bin CMD ["env"]
This
Dockerfile
first defined an argument namedTAG
with the default value of the latest. The next line is theFROM
directive, which will use the ubuntu parent image with theTAG
variable value sent with thebuild
command (or the default value if no value is sent with the build command). Then, theLABEL
directive sets the value for the maintainer. Next is theENV
directive, which defines the environment variable ofPUBLISHER
with the valuepackt
, andAPP_DIR
with the value of/usr/local/app/bin
. Finally, use theCMD
directive to execute theenv
command, which will print all the environment variables. - Now, build the Docker image:
$ docker image build -t env-arg --build-arg TAG=19.04 .
Note the
env-arg --build-arg TAG=19.04
flag used to send theTAG
argument to the build process. The output should be as follows:Figure 2.5: Building the env-arg Docker image
Note that the
19.04
tag of the ubuntu image was used as the parent image. This is because you sent the--build-arg flag
with the value ofTAG=19.04
during the build process. - Now, execute the
docker container run
command to start a new container from the Docker image that you built in the last step:$ docker container run env-arg
As we can see from the output, the
PUBLISHER
environment variable is available with the value ofpackt
, and theAPP_DIR
environment variable is available with the value of/usr/local/app/bin
:
Figure 2.6: Running the env-arg Docker container
In this exercise, we defined environment variables for a Docker image using the ENV
directive. We also experienced how to use ARG
directives to pass values during the Docker image build time. In the next section, we will be covering the WORKDIR
directive, which can be used to define the current working directory of the Docker container.
The WORKDIR Directive
The WORKDIR
directive is used to specify the current working directory of the Docker container. Any subsequent ADD
, CMD
, COPY
, ENTRYPOINT
, and RUN
directives will be executed in this directory. The WORKDIR
directive has the following format:
WORKDIR /path/to/workdir
If the specified directory does not exist, Docker will create this directory and make it the current working directory, which means this directive executes both mkdir
and cd
commands implicitly.
There can be multiple WORKDIR
directives in the Dockerfile
. If a relative path is provided in a subsequent WORKDIR
directive, that will be relative to the working directory set by the previous WORKDIR
directive:
WORKDIR /one WORKDIR two WORKDIR three RUN pwd
In the preceding example, we are using the pwd
command at the end of the Dockerfile
to print the current working directory. The output of the pwd
command will be /one/two/three
.
In the next section, we will discuss the COPY
directive that is used to copy files from the local filesystem to the Docker image filesystem.
The COPY Directive
During the Docker image build process, we may need to copy files from our local filesystem to the Docker image filesystem. These files can be source code files (for example, JavaScript files), configuration files (for example, properties files), or artifacts (for example, JAR files). The COPY
directive can be used to copy files and folders from the local filesystem to the Docker image during the build process. This directive takes two arguments. The first one is the source path from the local filesystem, and the second one is the destination path on the image filesystem:
COPY <source> <destination>
In the following example, we are using the COPY
directive to copy the index.html
file from the local filesystem to the /var/www/html/
directory of the Docker image:
COPY index.html /var/www/html/index.html
Wildcards can also be specified to copy all files that match the given pattern. The following example will copy all files with the .html
extension from the current directory to the /var/www/html/
directory of the Docker image:
COPY *.html /var/www/html/
In addition to copying files, the --chown
flag can be used with the COPY
directive to specify the user and group ownership of the files:
COPY --chown=myuser:mygroup *.html /var/www/html/
In the preceding example, in addition to copying all the HTML files from the current directory to the /var/www/html/
directory, the --chown
flag is used to set file ownership, with the user as myuser
and group as mygroup
:
Note
The --chown
flag is only supported from Docker version 17.09 and above. For Docker versions below 17.09, you need to run the chown
command after the COPY
command to change file ownership.
In the next section, we will look at the ADD
directive.
The ADD Directive
The ADD
directive is also similar to the COPY
directive, and has the following format:
ADD <source> <destination>
However, in addition to the functionality provided by the COPY
directive, the ADD
directive also allows us to use a URL as the <source>
parameter:
ADD http://sample.com/test.txt /tmp/test.txt
In the preceding example, the ADD
directive will download the test.txt
file from http://sample.com
and copy the file to the /tmp
directory of the Docker image filesystem.
Another feature of the ADD
directive is automatically extracting the compressed files. If we add a compressed file (gzip, bzip2, tar, and so on) to the <source>
parameter, the ADD
directive will extract the archive and copy the content to the image filesystem.
Imagine we have a compressed file named html.tar.gz
that contains index.html
and contact.html
files. The following command will extract the html.tar.gz
file and copy the index.html
and contact.html
files to the /var/www/html
directory:
ADD html.tar.gz /var/www/html
Since the COPY
and ADD
directives provide almost the same functionality, it is recommended to always use the COPY
directive unless you need the additional functionality (add from a URL or extract a compressed file) provided by the ADD
directive. This is because the ADD
directive provides additional functionality that can behave unpredictably if used incorrectly (for example, copying files when you want to extract, or extracting files when you want to copy).
In the next exercise, we are going to use the WORKDIR
, COPY
, and ADD
directives to copy files into the Docker image.
Exercise 2.04: Using the WORKDIR, COPY, and ADD Directives in the Dockerfile
In this exercise, you will deploy your custom HTML file to the Apache web server. You will use Ubuntu as the base image and install Apache on top of it. Then, you will copy your custom index.html file to the Docker image and download the Docker logo (from the https://www.docker.com website) to be used with the custom index.html file:
- Create a new directory named
workdir-copy-add-exercise
using themkdir
command:mkdir workdir-copy-add-exercise
- Navigate to the newly created
workdir-copy-add-exercise
directory:cd workdir-copy-add-exercise
- Within the
workdir-copy-add-exercise
directory, create a file namedindex.html
. This file will be copied to the Docker image during build time:touch index.html
- Now, open
index.html
using your favorite text editor:vim index.html
- Add the following content to the
index.html
file, save it, and exit fromindex.html
:<html> <body> <h1>Welcome to The Docker Workshop</h1> <img src="logo.png" height="350" width="500"/> </body> </html>
This HTML file will output
Welcome to The Docker Workshop
as the header of the page andlogo.png
(which we will download during the Docker image build process) as an image. You have defined the size of thelogo.png
image as a height of350
and a width of500
. - Within the
workdir-copy-add-exercise
directory, create a file namedDockerfile
:touch Dockerfile
- Now, open the
Dockerfile
using your favorite text editor:vim Dockerfile
- Add the following content to the
Dockerfile
, save it, and exit from theDockerfile
:# WORKDIR, COPY and ADD example FROM ubuntu:latest RUN apt-get update && apt-get install apache2 -y WORKDIR /var/www/html/ COPY index.html . ADD https://www.docker.com/sites/default/files/d8/2019-07/ Moby-logo.png ./logo.png CMD ["ls"]
This
Dockerfile
first defines the ubuntu image as the parent image. The next line is theRUN
directive, which will executeapt-get update
to update the package list, andapt-get install apache2 -y
to install the Apache HTTP server. Then, you will set/var/www/html/
as the working directory. Next, copy theindex.html
file that we created in step 3 to the Docker image. Then, use theADD
directive to download the Docker logo from https://www.docker.com/sites/default/files/d8/2019-07/Moby-logo.png to the Docker image. The final step is to use thels
command to print the content of the/var/www/html/
directory. - Now, build the Docker image with the tag of
workdir-copy-add
:$ docker image build -t workdir-copy-add .
You will observe that the image is successfully built and tagged as
latest
since we did not explicitly tag our image:Figure 2.7: Building the Docker image using WORKDIR, COPY, and ADD directives
- Execute the
docker container run
command to start a new container from the Docker image that you built in the previous step:$ docker container run workdir-copy-add
As we can see from the output, both the
index.html
andlogo.png
files are available in the/var/www/html/
directory:index.html logo.png
In this exercise, we observed how the WORKDIR
, ADD
, and COPY
directives work with Docker. In the next section, we are going to discuss the USER
directive.
The USER Directive
Docker will use the root user as the default user of a Docker container. We can use the USER
directive to change this default behavior and specify a non-root user as the default user of a Docker container. This is a great way to improve security by running the Docker container as a non-privileged user. The username specified with the USER
directive will be used to run all subsequent RUN
, CMD
, and ENTRYPOINT
directives in the Dockerfile
.
The USER
directive takes the following format:
USER <user>
In addition to the username, we can also specify the optional group name to run the Docker container:
USER <user>:<group>
We need to make sure that the <user>
and <group>
values are valid user and group names. Otherwise, the Docker daemon will throw an error while trying to run the container:
docker: Error response from daemon: unable to find user my_user: no matching entries in passwd file.
Now, let's try our hands at using the USER
directive in the next exercise.
Exercise 2.05: Using USER Directive in the Dockerfile
Your manager has asked you to create a Docker image to run the Apache web server. He has specifically requested that you use a non-root user while running the Docker container due to security reasons. In this exercise, you will use the USER
directive in the Dockerfile
to set the default user. You will be installing the Apache web server and changing the user to www-data
. Finally, you will execute the whoami
command to verify the current user by printing the username:
Note
The www-data
user is the default user for the Apache web server on Ubuntu.
- Create a new directory named
user-exercise
for this exercise:mkdir user-exercise
- Navigate to the newly created
user-exercise
directory:cd user-exercise
- Within the
user-exercise
directory, create a file namedDockerfile
:touch Dockerfile
- Now, open the
Dockerfile
using your favorite text editor:vim Dockerfile
- Add the following content to the
Dockerfile
, save it, and exit from theDockerfile
:# USER example FROM ubuntu RUN apt-get update && apt-get install apache2 -y USER www-data CMD ["whoami"]
This
Dockerfile
first defines the Ubuntu image as the parent image. The next line is theRUN
directive, which will executeapt-get update
to update the package list, andapt-get install apache2 -y
to install the Apache HTTP server. Next, you use theUSER
directive to change the current user to thewww-data
user. Finally, you have theCMD
directive, which executes thewhoami
command, which will print the username of the current user. - Build the Docker image:
$ docker image build -t user .
The output should be as follows:
Figure 2.8: Building the user Docker image
- Now, execute the
docker container
run command to start a new container from the Docker image that we built in the previous step:$ docker container run user
As you can see from the following output,
www-data
is the current user associated with the Docker container:www-data
In this exercise, we implemented the USER
directive in the Dockerfile
to set the www-data
user as the default user of the Docker image.
In the next section, we will discuss the VOLUME
directive.
The VOLUME Directive
In Docker, the data (for example, files, executables) generated and used by Docker containers will be stored within the container filesystem. When we delete the container, all the data will be lost. To overcome this issue, Docker came up with the concept of volumes. Volumes are used to persist the data and share the data between containers. We can use the VOLUME
directive within the Dockerfile
to create Docker volumes. Once a VOLUME
is created in the Docker container, a mapping directory will be created in the underlying host machine. All file changes to the volume mount of the Docker container will be copied to the mapped directory of the host machine.
The VOLUME
directive generally takes a JSON array as the parameter:
VOLUME ["/path/to/volume"]
Or, we can specify a plain string with multiple paths:
VOLUME /path/to/volume1 /path/to/volume2
We can use the docker container inspect <container>
command to view the volumes available in a container. The output JSON of the docker container inspect command will print the volume information similar to the following:
"Mounts": [ { "Type": "volume", "Name": "77db32d66407a554bd0dbdf3950671b658b6233c509ea ed9f5c2a589fea268fe", "Source": "/var/lib/docker/volumes/77db32d66407a554bd0 dbdf3950671b658b6233c509eaed9f5c2a589fea268fe/_data", "Destination": "/path/to/volume", "Driver": "local", "Mode": "", "RW": true, "Propagation": "" } ],
As per the preceding output, there is a unique name given to the volume by Docker. Also, the source and destination paths of the volume are mentioned in the output.
Additionally, we can execute the docker volume inspect <volume>
command to display detailed information pertaining to a volume:
[ { "CreatedAt": "2019-12-28T12:52:52+05:30", "Driver": "local", "Labels": null, "Mountpoint": "/var/lib/docker/volumes/77db32d66407a554 bd0dbdf3950671b658b6233c509eaed9f5c2a589fea268fe/_data", "Name": "77db32d66407a554bd0dbdf3950671b658b6233c509eae d9f5c2a589fea268fe", "Options": null, "Scope": "local" } ]
This is also similar to the previous output, with the same unique name and the mount path of the volume.
In the next exercise, we will learn how to use the VOLUME
directive in a Dockerfile
.
Exercise 2.06: Using VOLUME Directive in the Dockerfile
In this exercise, you will be setting a Docker container to run the Apache web server. However, you do not want to lose the Apache log files in case of a Docker container failure. As a solution, you have decided to persist in the log files by mounting the Apache log path to the underlying Docker host:
- Create a new directory named
volume-exercise
:mkdir volume-exercise
- Navigate to the newly created
volume-exercise
directory:cd volume-exercise
- Within the
volume-exercise
directory, create a file namedDockerfile
:touch Dockerfile
- Now, open the
Dockerfile
using your favorite text editor:vim Dockerfile
- Add the following content to the
Dockerfile
, save it, and exit from theDockerfile
:# VOLUME example FROM ubuntu RUN apt-get update && apt-get install apache2 -y VOLUME ["/var/log/apache2"]
This
Dockerfile
started by defining the Ubuntu image as the parent image. Next, you will execute theapt-get update
command to update the package list, and theapt-get install apache2 -y
command to install the Apache web server. Finally, use theVOLUME
directive to set up a mount point to the/var/log/apache2
directory. - Now, build the Docker image:
$ docker image build -t volume .
The output should be as follows:
Figure 2.9: Building the volume Docker image
- Execute the docker container run command to start a new container from the Docker image that you built in the previous step. Note that you are using the
--interactive
and--tty
flags to open an interactive bash session so that you can execute commands from the bash shell of the Docker container. You have also used the--name
flag to define the container name asvolume-container
:$ docker container run --interactive --tty --name volume-container volume /bin/bash
Your bash shell will be opened as follows:
root@bc61d46de960: /#
- From the Docker container command line, change directory to the
/var/log/apache2/
directory:# cd /var/log/apache2/
This will produce the following output:
root@bc61d46de960: /var/log/apache2#
- Now, list the available files in the directory:
# ls -l
The output should be as follows:
Figure 2.10: Listing files of the /var/log/apache2 directory
These are the log files created by Apache while running the process. The same files should be available once you check the host mount of this volume.
- Now, exit the container to check the host filesystem:
# exit
- Inspect
volume-container
to view the mount information:$ docker container inspect volume-container
Under the "
Mounts
" key, you can see the information relating to the mount:Figure 2.11: Inspecting the Docker container
- Inspect the volume with the
docker volume inspect <volume_name>
command.<volume_name>
can be identified by theName
field of the preceding output:$ docker volume inspect 354d188e0761d82e1e7d9f3d5c6ee644782b7150f51cead8f140556e5d334bd5
You should get the output similar to the following:
Figure 2.12: Inspecting the Docker volume
We can see that the container is mounted to the host path of
"/var/lib/docker/volumes/354d188e0761d82e1e7d9f3d5c6ee644782b 7150f51cead8f140556e5d334bd5/_data"
, which is defined as theMountpoint
field in the preceding output. - List the files available in the host file path. The host file path can be identified with the
"Mountpoint
" field of the preceding output:$ sudo ls -l /var/lib/docker/volumes/354d188e0761d82e1e7d9f3d5c6ee644782b7150f51cead8f14 0556e5d334bd5/_data
In the following output, you can see that the log files in the
/var/log/apache2
directory of the container are mounted to the host:
Figure 2.13: Listing files in the mount point directory
In this exercise, we observed how to mount the log path of the Apache web server to the host filesystem using the VOLUME
directive. In the next section, we will learn about the EXPOSE
directive.
The EXPOSE Directive
The EXPOSE
directive is used to inform Docker that the container is listening on the specified ports at runtime. We can use the EXPOSE
directive to expose ports through either TCP or UDP protocols. The EXPOSE
directive has the following format:
EXPOSE <port>
However, the ports exposed with the EXPOSE
directive will only be accessible from within the other Docker containers. To expose these ports outside the Docker container, we can publish the ports with the -p
flag with the docker container run
command:
docker container run -p <host_port>:<container_port> <image>
As an example, imagine that we have two containers. One is a NodeJS web app container that should be accessed from outside via port 80
. The second one is the MySQL container, which should be accessed from the node app container via port 3306
. In this scenario, we have to expose port 80
of the NodeJS app with the EXPOSE
directive and use the -p
flag with the docker container run
command to expose it externally. However, for the MySQL container, we can only use the EXPOSE
directive without the -p
flag when running the container, as 3306
should only be accessible from the node app container.
So, in summary, the following statements define this directive:
- If we specify both the
EXPOSE
directive and-p
flag, exposed ports will be accessible from other containers as well as externally. - If we specify
EXPOSE
without the-p
flag, exposed ports will only be accessible from other containers, but not externally.
You will learn about the HEALTHCHECK
directive in the next section.
The HEALTHCHECK Directive
Health checks are used in Docker to check whether the containers are running healthily. For example, we can use health checks to make sure the application is running within the Docker container. Unless there is a health check specified, there is no way for Docker to say whether a container is healthy. This is very important if you are running Docker containers in production environments. The HEALTHCHECK
directive has the following format:
HEALTHCHECK [OPTIONS] CMD command
There can be only one HEALTHCHECK
directive in a Dockerfile
. If there is more than one HEALTHCHECK
directive, only the last one will take effect.
As an example, we can use the following directive to ensure that the container can receive traffic on the http://localhost/
endpoint:
HEALTHCHECK CMD curl -f http://localhost/ || exit 1
The exit code at the end of the preceding command is used to specify the health status of the container. 0
and 1
are valid values for this field. 0 is used to denote a healthy container, and 1
is used to denote an unhealthy container.
In addition to the command, we can specify a few other parameters with the HEALTHCHECK
directive, as follows:
--interval
: This specifies the period between each health check (the default is 30s).--timeout
: If no success response is received within this period, the health check is considered failed (the default is 30s).--start-period
: The duration to wait before running the first health check. This is used to give a startup time for the container (the default is 0s).--retries
: The container will be considered unhealthy if the health check failed consecutively for the given number of retries (the default is 3).
In the following example, we have overridden the default values by providing our custom values with the HEALTHCHECK
directive:
HEALTHCHECK --interval=1m --timeout=2s --start-period=2m --retries=3 \ CMD curl -f http://localhost/ || exit 1
We can check the health status of a container with the docker container list
command. This will list the health status under the STATUS
column:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d4e627acf6ec sample "apache2ctl -D FOREG…" About a minute ago Up About a minute (healthy) 0.0.0.0:80->80/tcp upbeat_banach
As soon as we start the container, the health status will be health: starting. Following the successful execution of the HEALTHCHECK
command, the status will change to healthy
.
In the next exercise, we are going to use the EXPOSE
and HEALTHCHECK
directives to create a Docker container with the Apache web server and define health checks for it.
Exercise 2.07: Using EXPOSE and HEALTHCHECK Directives in the Dockerfile
Your manager has asked you to dockerize the Apache web server to access the Apache home page from the web browser. Additionally, he has asked you to configure health checks to determine the health status of the Apache web server. In this exercise, you will use the EXPOSE
and HEALTHCHECK
directives to achieve this goal:
- Create a new directory named
expose-healthcheck
:mkdir expose-healthcheck
- Navigate to the newly created
expose-healthcheck
directory:cd expose-healthcheck
- Within the
expose-healthcheck
directory, create a file namedDockerfile
:touch Dockerfile
- Now, open the
Dockerfile
using your favorite text editor:vim Dockerfile
- Add the following content to the
Dockerfile
, save it, and exit from theDockerfile
:# EXPOSE & HEALTHCHECK example FROM ubuntu RUN apt-get update && apt-get install apache2 curl -y HEALTHCHECK CMD curl -f http://localhost/ || exit 1 EXPOSE 80 ENTRYPOINT ["apache2ctl", "-D", "FOREGROUND"]
This
Dockerfile
first defines the ubuntu image as the parent image. Next, we execute theapt-get update
command to update the package list, and theapt-get install apache2 curl -y
command to install the Apache web server and curl tool.Curl
is required to execute theHEALTHCHECK
command. Next, we define theHEALTHCHECK
directive with curl to thehttp://localhost/
endpoint. Then, we exposed port80
of the Apache web server so that we can access the home page from our web browser. Finally, we start the Apache web server with theENTRYPOINT
directive. - Now, build the Docker image:
$ docker image build -t expose-healthcheck.
You should get the following output:
Figure 2.14: Building the expose-healthcheck Docker image
- Execute the docker container run command to start a new container from the Docker image that you built in the previous step. Note that you are using the
-p
flag to redirect port80
of the host to port80
of the container. Additionally, you have used the--name
flag to specify the container name asexpose-healthcheck-container
, and the-d
flag to run the container in detached mode (this runs the container in the background):$ docker container run -p 80:80 --name expose-healthcheck-container -d expose-healthcheck
- List the running containers with the
docker container list
command:$ docker container list
In the following output, you can see that the
STATUS
of theexpose-healthcheck-container
is healthy:Figure 2.15: List of running containers
- Now, you should be able to view the Apache home page. Go to the
http://127.0.0.1
endpoint from your favorite web browser:Figure 2.16: Apache home page
- Now, clean up the container. First, stop the Docker container by using the
docker container stop
command:$ docker container stop expose-healthcheck-container
- Finally, remove the Docker container with the
docker container rm
command:$ docker container rm expose-healthcheck-container
In this exercise, you utilized the EXPOSE
directive to expose an Apache web server as a Docker container and used the HEALTHCHECK
directive to define a health check to verify the healthy status of the Docker container.
In the next section, we will learn about the ONBUILD
directive.
The ONBUILD Directive
The ONBUILD
directive is used in the Dockerfile
to create a reusable Docker image that will be used as the base for another Docker image. As an example, we can create a Docker image that contains all the prerequisites, such as dependencies and configurations, in order to run an application. Then, we can use this 'prerequisite' image as the parent image to run the application.
While creating the prerequisite image, we can use the ONBUILD
directive, which will include the instructions that should only be executed when this image is used as the parent image in another Dockerfile
. ONBUILD
instructions will not be executed while building the Dockerfile
that contains the ONBUILD
directive, but only when building the child image.
The ONBUILD
directive takes the following format:
ONBUILD <instruction>
As an example, consider that we have the following ONBUILD
instruction in the Dockerfile
of our custom base image:
ONBUILD ENTRYPOINT ["echo","Running ONBUILD directive"]
The "Running ONBUILD directive"
value will not be printed if we create a Docker container from our custom base image. However, the "Running ONBUILD directive"
value will be printed if we use our custom base image as the base for our new child Docker image.
We can use the docker image inspect
command for the parent image to list the OnBuild triggers listed for the image:
$ docker image inspect <parent-image>
The command will return output similar to the following:
... "OnBuild": [ "CMD [\"echo\",\"Running ONBUILD directive\"]" ] ...
In the next exercise, we will be using the ONBUILD
directive to define a Docker image to deploy the HTML files.
Exercise 2.08: Using ONBUILD Directive in the Dockerfile
You have been asked by your manager to create a Docker image that is capable of running any HTML files provided by the software development team. In this exercise, you will build a parent image with the Apache web server and use the ONBUILD
directive to copy the HTML files. The software development team can use this Docker image as the parent image to deploy and test any HTML files created by them:
- Create a new directory named
onbuild-parent
:mkdir onbuild-parent
- Navigate to the newly created
onbuild-parent
directory:cd onbuild-parent
- Within the
onbuild-parent
directory, create a file namedDockerfile
:touch Dockerfile
- Now, open the
Dockerfile
using your favorite text editor:vim Dockerfile
- Add the following content to the
Dockerfile
, save it, and exit from theDockerfile
:# ONBUILD example FROM ubuntu RUN apt-get update && apt-get install apache2 -y ONBUILD COPY *.html /var/www/html EXPOSE 80 ENTRYPOINT ["apache2ctl", "-D", "FOREGROUND"]
This
Dockerfile
first defines the ubuntu image as the parent image. It then executes theapt-get update
command to update the package list, and theapt-get install apache2 -y
command to install the Apache web server. TheONBUILD
directive is used to provide a trigger to copy all HTML files to the/var/www/html
directory. TheEXPOSE
directive is used to expose port80
of the container andENTRYPOINT
to start the Apache web server using theapache2ctl
command. - Now, build the Docker image:
$ docker image build -t onbuild-parent .
The output should be as follows:
Figure 2.17: Building the onbuild-parent Docker image
- Execute the
docker container run
command to start a new container from the Docker image built in the previous step:$ docker container run -p 80:80 --name onbuild-parent-container -d onbuild-parent
In the preceding command, you have started the Docker container in detached mode while exposing port
80
of the container. - Now, you should be able to view the Apache home page. Go to the
http://127.0.0.1
endpoint from your favorite web browser. Note that the default Apache home page is visible:Figure 2.18: Apache home page
- Now, clean up the container. Stop the Docker container by using the
docker container stop
command:$ docker container stop onbuild-parent-container
- Remove the Docker container with the
docker container rm
command:$ docker container rm onbuild-parent-container
- Now, create another Docker image using
onbuild-parent-container
as the parent image to deploy a custom HTML home page. First, change the directory back to the previous directory:cd ..
- Create a new directory named
onbuild-child
for this exercise:mkdir onbuild-child
- Navigate to the newly created
onbuild-child
directory:cd onbuild-child
- Within the
onbuild-child
directory, create a file namedindex.html
. This file will be copied to the Docker image by theONBUILD
command during build time:touch index.html
- Now, open the
index.html
file using your favorite text editor:vim index.html
- Add the following content to the
index.html
file, save it, and exit from theindex.html
file:<html> <body> <h1>Learning Docker ONBUILD directive</h1> </body> </html>
This is a simple HTML file that will output the
Learning Docker ONBUILD
directive as the header of the page. - Within the
onbuild-child
directory, create a file namedDockerfile
:touch Dockerfile
- Now, open the
Dockerfile
using your favorite text editor:vim Dockerfile
- Add the following content to the
Dockerfile
, save it, and exit from theDockerfile
:# ONBUILD example FROM onbuild-parent
This
Dockerfile
has only one directive. This will use theFROM
directive to utilize theonbuild-parent
Docker image that you created previously as the parent image. - Now, build the Docker image:
$ docker image build -t onbuild-child .
Figure 2.19: Building the onbuild-child Docker image
- Execute the
docker container run
command to start a new container from the Docker image that you built in the previous step:$ docker container run -p 80:80 --name onbuild-child-container -d onbuild-child
In this command, you have started the Docker container from the
onbuild-child
Docker image while exposing port80
of the container. - You should be able to view the Apache home page. Go to the
http://127.0.0.1
endpoint from your favorite web browser:Figure 2.20: Customized home page of the Apache web server
- Now, clean up the container. First, stop the Docker container by using the
docker container stop
command:$ docker container stop onbuild-child-container
- Finally, remove the Docker container with the
docker container rm
command:$ docker container rm onbuild-child-container
In this exercise, we observed how we can use the ONBUILD
directive to create a reusable Docker image that is capable of running any HTML file provided to it. We created the reusable Docker image named onbuild-parent
with the Apache web server, with port 80
exposed. This Dockerfile
contains the ONBUILD
directive to copy the HTML files in the context of the Docker image. Then, we created the second Docker image named onbuild-child
, using onbuild-parent
as the base image, that provided a simple HTML file to be deployed to the Apache web server.
Now, let's test our knowledge that we have acquired in this chapter by dockerizing the given PHP application using the Apache web server in the following activity.
Activity 2.01: Running a PHP Application on a Docker Container
Imagine that you want to deploy a PHP welcome page that will greet visitors based on the date and time using the following logic. Your task is to dockerize the PHP application given here, using the Apache web server installed on an Ubuntu base image:
<?php $hourOfDay = date('H'); if($hourOfDay < 12) { $message = "Good Morning"; } elseif($hourOfDay > 11 && $hourOfDay < 18) { $message = "Good Afternoon"; } elseif($hourOfDay > 17){ $message = "Good Evening"; } echo $message; ?>
This is a simple PHP file that will greet the user based on the following logic:

Figure 2.21: Logic of PHP application
Execute the following steps to complete this activity:
- Create a folder to store the activity files.
- Create a
welcome.php
file with the code provided previously. - Create a
Dockerfile
and set up the application with PHP and Apache2 on an Ubuntu base image. - Build and run the Docker image.
- Once completed, stop and remove the Docker container.
Note
The solution for this activity can be found via this link.
Summary
In this chapter, we discussed how we can use a Dockerfile
to create our own custom Docker images. First, we discussed what is a Dockerfile
and the syntax of a Dockerfile
. We then discussed some common Docker directives, including the FROM
, LABEL
, RUN
, CMD
, and ENTRYPOINT
directives. Then, we created our first Dockerfile
with the common directives that we learned.
In the next section, we focused on building Docker images. We discussed multiple areas in depth regarding Docker images, including the layered filesystem of Docker images, the context in Docker builds, and the use of the cache during the Docker build process. Then, we discussed more advanced Dockerfile
directives, including the ENV
, ARG
, WORKDIR
, COPY
, ADD
, USER
, VOLUME
, EXPOSE
, HEALTHCHECK
, and ONBUILD
directives.
In the next chapter, we will discuss what a Docker registry is, look at private and public Docker registries, and learn how we can publish Docker images to Docker registries.