A common challenge that developers face when working with code is the ability to ensure that their code is tested in a consistent environment. Another challenge is for the operations teams to ensure that the environments are quickly built and stable. In DevOps, the ability to continually integrate features and fixes rapidly and efficiently is paramount, and ensuring that code is always tested in a consistent environment is essential.

There are many benefits of using containers. Developers use containers to provide applications with a consistent environment for testing. Containers are also lightweight compared with virtual machines, so their storage size is smaller and boots quickly. This allows containers to be ephemeral (quickly spun up and then torn down), making them excellent for scaling and portability.

This tutorial will work with Docker, but the goal is to help become more familiar with containerization. Be aware that Docker for macOS and Windows has a licensing fee for commercial use. Docker is one of many other container runtimes and tools such as Podman and Buildah, which are open source.

What You’ll Learn

What You’ll Need

A container is an isolated process on a host that shares the host operating system’s kernel. However, it runs sequestered from other instances on the host system, using its own binaries and libraries to support whichever application is running on it.

Beginners tend to confuse the concept of virtual machines and containers because they are both instanced environments that function on a physical host. However, virtual machines are made of complete operating systems running over a hypervisor. The virtual machine doesn’t share a kernel with the hypervisor as it has its own, just like a physical host; it is just virtualized.

Virtual machines are resource intensive and take longer to boot. On the other hand, containers don’t need to boot up, load drivers, and have their own kernel. They are much more lightweight, take up fewer resources, and boot quickly. This advantage makes containers perfect for being ephemeral environments for applications to run in when it comes to portability and testing.

containers-versus-vm-vs-baremetal-image

Installation

Before starting, you must have Docker or Podman installed on your host. It is OK if you do not have Docker installed on your system. I will provide a few links to walk you through the installation process.

Installing Docker is not difficult and should only take a few minutes to do. However, you will need sufficient privileges to install it. Podman is an excellent alternative to Docker because it doesn’t require admin privileges to run. Its front-end API structure is identical to Docker, making Podman manageable to use with a quick learning curve. Docker commands will work for Podman. You prepend the command with podman instead of docker; however, if you wish, you can make an alias for podman and change it to docker. You will not notice a difference on the front end.

Below are links to the official installation instructions on Docker.com. Docker Desktop requires a license for use by organizations with more than 250 employees or more than $10 million in revenue effective January 1, 2022. On the Windows and macOS operating systems, you can only use Docker Desktop. You may also use Podman as an alternative to Docker if you use Linux or macOS.

Verifying Docker and Reviewing the Dockerfile

First, I will verify that I have Docker installed in my environment with a simple version check. The output may not be the same version displayed below, but if Docker is installed, you should have similar output.

$ docker -v
Docker version 20.10.17, build 100c701

Let‘s clone the application to the directory and go to the working directory of the cloned repository.

$ git clone https://github.com/CiscoLearning/create-container-for-application.git
Cloning into 'create-container-for-application'...
remote: Enumerating objects: 52, done.
remote: Counting objects: 100% (52/52), done.
remote: Compressing objects: 100% (31/31), done.
remote: Total 52 (delta 17), reused 48 (delta 15), pack-reused 0
Receiving objects: 100% (52/52), 20.94 KiB | 5.23 MiB/s, done.
Resolving deltas: 100% (17/17), done.
$ cd tutorial-create-container-for-application/
~/tutorial-create-container-for-application$

If you take a quick look at the contents, you’ll see this repository has a Dockerfile. There are a couple of ways you can instruct Docker on how to assemble a container. This tutorial will focus on using a Dockerfile to build our container. A Dockerfile is a text file containing instructions that tell Docker which commands to run to create the container.

~/tutorial-create-container-for-application$ ls -al
total 64
drwxr-xr-x  13 barweiss  staff   416 Aug  1 16:43 .
drwxr-xr-x  14 barweiss  staff   448 Aug  1 16:43 ..
-rw-r--r--   1 barweiss  staff    55 Aug  1 16:43 .dockerignore
drwxr-xr-x  12 barweiss  staff   384 Aug  1 21:08 .git
-rw-r--r--   1 barweiss  staff    33 Aug  1 16:43 .gitignore
-rw-r--r--   1 barweiss  staff  2731 Aug  1 16:43 Dockerfile
-rw-r--r--   1 barweiss  staff  1070 Aug  1 16:43 LICENSE
-rw-r--r--   1 barweiss  staff   131 Aug  1 16:43 README.md
-rw-r--r--   1 barweiss  staff   469 Aug  1 16:43 main.py
-rw-r--r--   1 barweiss  staff   307 Aug  1 16:43 requirements.txt
drwxr-xr-x   5 barweiss  staff   160 Aug  1 16:43 static
-rw-r--r--   1 barweiss  staff   397 Aug  1 16:43 swanson_quote_api.py
drwxr-xr-x   3 barweiss  staff    96 Aug  1 16:43 templates

With your favorite code editor, open the Dockerfile to view the contents. I am going to use VS Code in the following examples.

~/tutorial-create-container-for-application$ code Dockerfile

Dockerfile-Screenshot

This Dockerfile is very simple but well documented and explains what each command does. (Note: Docker refers to these as instructions.) Take some time to read the Dockerfile and understand what it does. This Dockerfile tells Docker to use the python:3.10-slim-bullseye base image, a simple Python 3.10 shell. Next are a few labels that provide metadata for the image we are creating. In the next step, the Dockerfile creates a new directory in the container called /app and copies the contents in the local directory to the /app file in the new container. The WORKDIR sets the working directory for the application. Pip uses the requirements.txt file to load the Python dependencies into the environment, then opens TCP port 5000 for the container. The application uses TCP port 5000 for all incoming connections. The final line of the Dockerfile instructs Docker to run the command /app/main.py, which starts the application in the container.

In the table below are some more common Dockerfile commands you may encounter. Please see Docker’s Hello Build documentation for more details.

Common Dockerfile Instructions

InstructionDescription
ADDCopies a file from the host system onto the container
CMDThe command that runs when the container starts
ENTRYPOINTAllows you to configure a container that will run as an executable
ENVSets an environment variable in the new container
RUNExecutes a command and saves the result as a new layer
USERSets the default user within the container
VOLUMECreates a shared volume that can be shared among containers or by the host machine
WORKDIRSets the default working directory for the container
EXPOSEInforms Docker that the container listens on the specified network ports at runtime
LABELAdds metadata to an image in the form of a key/value pair

The .dockerignore File

The .dockerignore file is similar to the .gitignore file. Docker will not copy over any files covered in this file during the build process. The .dockerignore file can be used to keep files that may contain private or unique information from being inadvertently added to the container’s environment. A good example is an .env file that may have variables that are unique to a particular environment but are not desirable for the container’s environment.

The build Command

Once you have your application and Dockerfile configured, the build process is relatively simple to execute. The basics will be covered here, but I encourage you to review the documentation because there are quite a few considerations when going through this process.

First, ensure that you are at the root of your application’s directory. As you recall, in the previous step of this tutorial, we saw the Dockerfile located here. When using the build command, you will need to tell Docker where the Dockerfile is located, and as you will see, it will be easier for us to run the build command when we are in the same location.

The name and tag of the image are also needed for the docker build command. The -t argument in the docker build command allows you to add an <image name>:<tag> to the image, which will then be put into your local container repository. The tag is generally used to denote the version of your container. The naming and tagging conventions should follow a consistent policy, because it can become overwhelming to determine which image is the desired version. There are plenty of resources out there that discuss some of the best practices for container registry versioning and tagging.

Run the following command and be sure that you are running this command in the root of the application’s directory on the host.

~/tutorial-create-container-for-application$ docker build -t tao_of_ron_swanson:1.0 .

Notice the . at the end of the command; it lets Docker know that the Dockerfile is in this directory. After running the command, Docker will go through the process of building the image. Below is a sample output if the build is successful:

~/create-container-for-application$ docker build -t tao_of_ron_swanson:1.0 .
[+] Building 9.1s (9/9) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                 0.0s
 => => transferring dockerfile: 2.82kB                                                                                                                               0.0s
 => [internal] load .dockerignore                                                                                                                                    0.0s
 => => transferring context: 95B                                                                                                                                     0.0s
 => [internal] load metadata for docker.io/library/python:3.10-slim-bullseye                                                                                         0.0s
 => [internal] load build context                                                                                                                                    0.0s
 => => transferring context: 78.90kB                                                                                                                                 0.0s
 => CACHED [1/4] FROM docker.io/library/python:3.10-slim-bullseye                                                                                                    0.0s
 => [2/4] COPY . /app                                                                                                                                                0.0s
 => [3/4] WORKDIR /app                                                                                                                                               0.0s
 => [4/4] RUN pip install -r requirements.txt                                                                                                                        8.8s
 => exporting to image                                                                                                                                               0.2s
 => => exporting layers                                                                                                                                              0.2s
 => => writing image sha256:713d63d683d153a20549599786c652ff89ae0741200c8322644b7c2d70afb90f                                                                         0.0s
 => => naming to docker.io/library/tao_of_ron_swanson:1.0

Verifying and Running the Image

Once the image has been built, verify it is in the local repository.

~/create-container-for-application$ docker images tao_of_ron_swanson:1.0
REPOSITORY           TAG       IMAGE ID       CREATED          SIZE
tao_of_ron_swanson   1.0       713d63d683d1   13 minutes ago   140MB

The docker images command will display all the images in your local container repository. Here, we knew which image to look for, and to make the output easier to read, we added the name and tag of the image to filter in the output.

To run a container, we use the docker run command (see the documentation for more information). We will use the arguments -dit to instruct Docker to run the container detached in the background and as an interactive open process; otherwise, it just runs once and exits as soon as the Python main.py script completes. The -t argument allocates a pseudo-terminal session, which is good if we need to access the container for troubleshooting. The -p argument is also required to let Docker know we want to externally open TCP port 5000 in this instance. Here, we map the external port 5000 to the container’s port 5000. If we were running multiple containers, the external port number needs to change because port 5000 would already be in use. So, if a second container were running, you would see an argument as -p 5001:5000, which means map external port 5001 to the container’s port 5000. Finally, you can add an optional name to this running container with the --name argument; otherwise, Docker will generate a random name for the new container. The image name is added at the very end.

~/create-container-for-application$ docker run -dit -p 5000:5000 --name example_image tao_of_ron_swanson:1.0
ccdf526fafa941db9c4e3e17ba58151ccbad6830df5e8dbfbf62f636d39c4d69

If the command is successful, the output will be assigned a hashed ID in hex for the Docker container instance. The nice thing about Docker is that you can use the first few hash values to identify the container. If no other container has ccdf as the first four values of its hash, then you can use ccdf to call on that container. In the example below, we will use the docker ps command to verify that this container is running. We will use the -f filter argument again to search by the first four hex values of its ID.

~/create-container-for-application$ docker ps -f id=ccdf
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS                    NAMES
ccdf526fafa9   tao_of_ron_swanson:1.0   "/bin/sh -c 'python …"   14 minutes ago   Up 13 minutes   0.0.0.0:5000->5000/tcp   example_image

The output from the docker ps command provides information about the image’s current state. We can see that the container was created 14 minutes ago and has been up for about 13 minutes. We also see that the external localhost port 5000 is forwarding to 5000/tcp on the container.

Our application is now up and running in a containerized instance. The output of this application can be viewed by opening your browser and going to http://localhost:5000.

application-output-in-the-browser

This tutorial has just scratched the surface of Docker and containerization. If you would like to learn more, please visit the following links: