Book Image

The Docker Workshop

By : Vincent Sesto, Onur Yılmaz, Sathsara Sarathchandra, Aric Renzo, Engy Fouda
5 (3)
Book Image

The Docker Workshop

5 (3)
By: Vincent Sesto, Onur Yılmaz, Sathsara Sarathchandra, Aric Renzo, Engy Fouda

Overview of this book

No doubt Docker Containers are the future of highly-scalable software systems and have cost and runtime efficient supporting infrastructure. But learning it might look complex as it comes with many technicalities. This is where The Docker Workshop will help you. Through this workshop, you’ll quickly learn how to work with containers and Docker with the help of practical activities.? The workshop starts with Docker containers, enabling you to understand how it works. You’ll run third party Docker images and also create your own images using Dockerfiles and multi-stage Dockerfiles. Next, you’ll create environments for Docker images, and expedite your deployment and testing process with Continuous Integration. Moving ahead, you’ll tap into interesting topics and learn how to implement production-ready environments using Docker Swarm. You’ll also apply best practices to secure Docker images and to ensure that production environments are running at maximum capacity. Towards the end, you’ll gather skills to successfully move Docker from development to testing, and then into production. While doing so, you’ll learn how to troubleshoot issues, clear up resource bottlenecks and optimize the performance of services. By the end of this workshop, you’ll be able to utilize Docker containers in real-world use cases.
Table of Contents (17 chapters)
Preface

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:

  1. The ENV directive
  2. The ARG directive
  3. The WORKDIR directive
  4. The COPY directive
  5. The ADD directive
  6. The USER directive
  7. The VOLUME directive
  8. The EXPOSE directive
  9. The HEALTHCHECK directive
  10. 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:

  1. Create a new directory named env-arg-exercise using the mkdir command:
    mkdir env-arg-exercise
  2. Navigate to the newly created env-arg-exercise directory using the cd command:
    cd env-arg-exercise
  3. Within the env-arg-exercise directory, create a file named Dockerfile:
    touch Dockerfile
  4. Now, open the Dockerfile using your favorite text editor:
    vim Dockerfile
  5. Add the following content to the Dockerfile. Then, save and exit from the Dockerfile:
    # ENV and ARG example
    ARG TAG=latest
    FROM ubuntu:$TAG
    LABEL [email protected] 
    ENV PUBLISHER=packt APP_DIR=/usr/local/app/bin
    CMD ["env"]

    This Dockerfile first defined an argument named TAG with the default value of the latest. The next line is the FROM directive, which will use the ubuntu parent image with the TAG variable value sent with the build command (or the default value if no value is sent with the build command). Then, the LABEL directive sets the value for the maintainer. Next is the ENV directive, which defines the environment variable of PUBLISHER with the value packt, and APP_DIR with the value of /usr/local/app/bin. Finally, use the CMD directive to execute the env command, which will print all the environment variables.

  6. 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 the TAG argument to the build process. The output should be as follows:

    Figure 2.5: Building the env-arg Docker image

    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 of TAG=19.04 during the build process.

  7. 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 of packt, and the APP_DIR environment variable is available with the value of /usr/local/app/bin:

    Figure 2.6: Running the env-arg Docker container

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:

  1. Create a new directory named workdir-copy-add-exercise using the mkdir command:
    mkdir workdir-copy-add-exercise
  2. Navigate to the newly created workdir-copy-add-exercise directory:
    cd workdir-copy-add-exercise
  3. Within the workdir-copy-add-exercise directory, create a file named index.html. This file will be copied to the Docker image during build time:
    touch index.html 
  4. Now, open index.html using your favorite text editor:
    vim index.html 
  5. Add the following content to the index.html file, save it, and exit from index.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 and logo.png (which we will download during the Docker image build process) as an image. You have defined the size of the logo.png image as a height of 350 and a width of 500.

  6. Within the workdir-copy-add-exercise directory, create a file named Dockerfile:
    touch Dockerfile
  7. Now, open the Dockerfile using your favorite text editor:
    vim Dockerfile
  8. Add the following content to the Dockerfile, save it, and exit from the Dockerfile:
    # 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 the RUN directive, which will execute apt-get update to update the package list, and apt-get install apache2 -y to install the Apache HTTP server. Then, you will set /var/www/html/ as the working directory. Next, copy the index.html file that we created in step 3 to the Docker image. Then, use the ADD 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 the ls command to print the content of the /var/www/html/ directory.

  9. 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

    Figure 2.7: Building the Docker image using WORKDIR, COPY, and ADD directives

  10. 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 and logo.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.

  1. Create a new directory named user-exercise for this exercise:
    mkdir user-exercise
  2. Navigate to the newly created user-exercise directory:
    cd user-exercise
  3. Within the user-exercise directory, create a file named Dockerfile:
    touch Dockerfile
  4. Now, open the Dockerfile using your favorite text editor:
    vim Dockerfile
  5. Add the following content to the Dockerfile, save it, and exit from the Dockerfile:
    # 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 the RUN directive, which will execute apt-get update to update the package list, and apt-get install apache2 -y to install the Apache HTTP server. Next, you use the USER directive to change the current user to the www-data user. Finally, you have the CMD directive, which executes the whoami command, which will print the username of the current user.

  6. Build the Docker image:
    $ docker image build -t user .

    The output should be as follows:

    Figure 2.8: Building the user Docker image

    Figure 2.8: Building the user Docker image

  7. 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:

  1. Create a new directory named volume-exercise:
    mkdir volume-exercise
  2. Navigate to the newly created volume-exercise directory:
    cd volume-exercise
  3. Within the volume-exercise directory, create a file named Dockerfile:
    touch Dockerfile
  4. Now, open the Dockerfile using your favorite text editor:
    vim Dockerfile
  5. Add the following content to the Dockerfile, save it, and exit from the Dockerfile:
    # 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 the apt-get update command to update the package list, and the apt-get install apache2 -y command to install the Apache web server. Finally, use the VOLUME directive to set up a mount point to the /var/log/apache2 directory.

  6. Now, build the Docker image:
    $ docker image build -t volume .

    The output should be as follows:

    Figure 2.9: Building the volume Docker image

    Figure 2.9: Building the volume Docker image

  7. 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 as volume-container:
    $ docker container run --interactive --tty --name volume-container volume /bin/bash

    Your bash shell will be opened as follows:

    root@bc61d46de960: /#
  8. 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#
  9. 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

    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.

  10. Now, exit the container to check the host filesystem:
    # exit
  11. 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

    Figure 2.11: Inspecting the Docker container

  12. Inspect the volume with the docker volume inspect <volume_name> command. <volume_name> can be identified by the Name 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

    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 the Mountpoint field in the preceding output.

  13. 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

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:

  1. Create a new directory named expose-healthcheck:
    mkdir expose-healthcheck
  2. Navigate to the newly created expose-healthcheck directory:
    cd expose-healthcheck
  3. Within the expose-healthcheck directory, create a file named Dockerfile:
    touch Dockerfile
  4. Now, open the Dockerfile using your favorite text editor:
    vim Dockerfile
  5. Add the following content to the Dockerfile, save it, and exit from the Dockerfile:
    # 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 the apt-get update command to update the package list, and the apt-get install apache2 curl -y command to install the Apache web server and curl tool. Curl is required to execute the HEALTHCHECK command. Next, we define the HEALTHCHECK directive with curl to the http://localhost/ endpoint. Then, we exposed port 80 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 the ENTRYPOINT directive.

  6. 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

    Figure 2.14: Building the expose-healthcheck Docker image

  7. 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 port 80 of the host to port 80 of the container. Additionally, you have used the --name flag to specify the container name as expose-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
  8. List the running containers with the docker container list command:
    $ docker container list

    In the following output, you can see that the STATUS of the expose-healthcheck-container is healthy:

    Figure 2.15: List of running containers

    Figure 2.15: List of running containers

  9. 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

    Figure 2.16: Apache home page

  10. Now, clean up the container. First, stop the Docker container by using the docker container stop command:
    $ docker container stop expose-healthcheck-container
  11. 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:

  1. Create a new directory named onbuild-parent:
    mkdir onbuild-parent
  2. Navigate to the newly created onbuild-parent directory:
    cd onbuild-parent
  3. Within the onbuild-parent directory, create a file named Dockerfile:
    touch Dockerfile
  4. Now, open the Dockerfile using your favorite text editor:
    vim Dockerfile
  5. Add the following content to the Dockerfile, save it, and exit from the Dockerfile:
    # 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 the apt-get update command to update the package list, and the apt-get install apache2 -y command to install the Apache web server. The ONBUILD directive is used to provide a trigger to copy all HTML files to the /var/www/html directory. The EXPOSE directive is used to expose port 80 of the container and ENTRYPOINT to start the Apache web server using the apache2ctl command.

  6. 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

    Figure 2.17: Building the onbuild-parent Docker image

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

  8. 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

    Figure 2.18: Apache home page

  9. Now, clean up the container. Stop the Docker container by using the docker container stop command:
    $ docker container stop onbuild-parent-container
  10. Remove the Docker container with the docker container rm command:
    $ docker container rm onbuild-parent-container
  11. 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 ..
  12. Create a new directory named onbuild-child for this exercise:
    mkdir onbuild-child
  13. Navigate to the newly created onbuild-child directory:
    cd onbuild-child
  14. Within the onbuild-child directory, create a file named index.html. This file will be copied to the Docker image by the ONBUILD command during build time:
    touch index.html 
  15. Now, open the index.html file using your favorite text editor:
    vim index.html 
  16. Add the following content to the index.html file, save it, and exit from the index.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.

  17. Within the onbuild-child directory, create a file named Dockerfile:
    touch Dockerfile
  18. Now, open the Dockerfile using your favorite text editor:
    vim Dockerfile
  19. Add the following content to the Dockerfile, save it, and exit from the Dockerfile:
    # ONBUILD example
    FROM onbuild-parent

    This Dockerfile has only one directive. This will use the FROM directive to utilize the onbuild-parent Docker image that you created previously as the parent image.

  20. Now, build the Docker image:
    $ docker image build -t onbuild-child .
    Figure 2.19: Building the onbuild-child Docker image

    Figure 2.19: Building the onbuild-child Docker image

  21. 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 port 80 of the container.

  22. 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

    Figure 2.20: Customized home page of the Apache web server

  23. Now, clean up the container. First, stop the Docker container by using the docker container stop command:
    $ docker container stop onbuild-child-container
  24. 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

Figure 2.21: Logic of PHP application

Execute the following steps to complete this activity:

  1. Create a folder to store the activity files.
  2. Create a welcome.php file with the code provided previously.
  3. Create a Dockerfile and set up the application with PHP and Apache2 on an Ubuntu base image.
  4. Build and run the Docker image.
  5. Once completed, stop and remove the Docker container.

    Note

    The solution for this activity can be found via this link.