CHAPTER 5
Deploying Your Application Using Docker
This chapter aims to explain how to install Docker, how to work using it, create new containers, images, networks, and deploy the image inside a server.
Structure
In this chapter, we will discuss the following topics:
- Introduction to Docker
- Installing Docker and creating containers
- Creating images using Dockerfile
- Publishing images and deploying it
Objectives
After studying this unit, you should be able to:
- Understand what is Docker and how it works
- Setup a Docker environment
- Create containers and images
- Version images and deploying
Introduction to Docker
Docker is a tool set to create containers and version it. For those who have more experience with Linux, they probably already know what a container is by using the command chroot
on Linux. In simple words, a container is a directory in your file system that allows you to install another Linux operating system in it. As the command says chroot
(change root
), so the new root directory /
, will be the new one that you set. However, an entire operating system needs so much more than just the files. Then, for the networking part, the Docker uses iptables under the hood. To manage some redirects via NAT rules, I will show you the amount of stuff that Docker manages for you by running some simple commands in the following paragraphs.
Installation
Firstly, we need to install Docker in our environment. In the official documentation, you will find all the steps and the detailed instructions. Here, we have a summary of everything:
apt clean
apt-get update
apt-get remove docker docker-engine docker.io containerd runc -y
The preceding commands will remove any previous installation of Docker, if you had one in your machine:
apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common -y
Now, we are just installing some dependencies that are required for your Docker installation, like the apt-transport-https
, which is a requirement to download packages from the https
repositories:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
The following command will add GNU Privacy Guard
key to ensure the authenticity of the Docker repository:
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
We are using Ubuntu. So, the preceding command is specifically for Ubuntu environments:
apt-get update -y
apt-get install docker-ce docker-ce-cli containerd.io -y
To conclude, the previous command were to update your local repositories list and install the Docker Community Edition.
Creating Containers
The environment is ready. To ensure that your Docker is running fine, run the following command:
root@devops:~# systemctl status docker
docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2020-04-02 10:28:47 UTC; 1min 7s ago
Docs: https://docs.docker.com
Main PID: 6464 (dockerd)
Tasks: 9
CGroup: /system.slice/docker.service
└─6464 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
Now, your Docker installation is active and running. To create one container, you can just type the following command:
root@devops:~# docker run -ti centos /bin/bash
Unable to find image 'centos:latest' locally
latest: Pulling from library/centos
8a29a15cefae: Downloading [=================================================>] 72.58MB/73.23MB
The previous command means that we need to run a new container with a TTY (terminal), which is represented by the parameter -t
, and an interactive, represented by the parameter -i
. Thus, we can run the commands in the container terminal and see their outputs.
When your container is created, you can see the following output:
root@devops:~# docker run -ti centos /bin/bash
Unable to find image 'centos:latest' locally
latest: Pulling from library/centos
8a29a15cefae: Pull complete
Digest: sha256:fe8d824220415eed5477b63addf40fb06c3b049404242b31982106ac204f6700
Status: Downloaded newer image for centos:latest
[root@595b42fceebc /]#
The last line is the important one:
[root@595b42fceebc /]#
If you compare with our terminal in before running the command:
root@devops:~# docker run -ti centos /bin/bash
You can clearly see that the hostname is changed, which means now we are within the container and all the commands we will run, starting from now, will be running inside the container, and nothing will be installed in the VM environment. The hostname name given to the container 595b42fceebc
is the container ID, which is used to manage your container. To exit from your container, type: exit or Ctrl + D:
[root@595b42fceebc /]#
exit
root@devops:~#
To see your current running containers, type the following command:
We can see zero containers running. It happens because once you type exit or Ctrl + D from your container terminal, the container stops. To see that, type the following command:
Now, you can see your container created. If you give a look, you can see that the value in the column CONTAINER ID
is exactly the same as the container hostname when we created it.
However, why does the Docker has this behavior? Why is it that when we exit from a container, the container stops? This happens because in the containers concept, one container is created for one specific purpose, different from the virtual machines. So, we do not need to concern about uptime or maintaining a container. You can just delete it and create a new one. If you want to make changes into a container, you must modify the image, save it, delete the current container, and start a new one with the new version of the image.
In my case, I created a container based on a CentOS image, and I just ran the command /bin/bash
, which was keeping my container alive. Once that command stops running, the container becomes dead. It also applies for the container configurations. If you want to change a redirect or any other parameters, you must create a new container with those new parameters, save it, and run a new container.
Now, we will create a new container based in the CentOS image, and within this container, we will put a Python application that we learned in the last chapter:
root@devops:~# docker run -ti --name python_app -p 5000:5000 centos /bin/bash
[root@ce7f74b0304a /]#
The creation now was clearly faster than the first one. This happens because the CentOS image is already in our local repository. Therefore, the Docker does not need to download it again.
Install the dependencies in the container with the following command:
[root@ce7f74b0304a /]# yum clean all && yum install python3 python3-pip -y
Failed to set locale, defaulting to C.UTF-8
0 files removed
Failed to set locale, defaulting to C.UTF-8
CentOS-8 - AppStream 11% [=======] 122 kB/s | 767 kB 00:49 ETA
While the download is running, we have a new command now, the yum
. This command is respective to APT on Ubuntu, and you can use it to install the packages from remote repositories.
After the download finishes, install the Python modules for the application:
[root@ce7f74b0304a /]# python3 -m pip install flask
Successfully installed Jinja2-2.11.1 MarkupSafe-1.1.1 Werkzeug-1.0.1 click-7.1.1 flask-1.1.1 itsdangerous-1.1.0
Copy the source code used in the previous chapter to the container:
[root@ce7f74b0304a /]# cat <<EOF > /srv/app.py
> from flask import Flask
>
>
> @app.route("/")
> def index():
> return "DevOps with Linux"
>
>
> if __name__ == "__main__":
> app.run(debug=True,host="0.0.0.0")
> EOF
Run the application to see if it is working:
[root@ce7f74b0304a /]# python3 /srv/app.py
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 123-083-749
Perfect! Now, you can test if from the web browser:
Figure 5.1
Everything is running fine. Now, we have a container running, with an application inside it. The dependencies were installed and we already know the command to run the application. Now, we need to create a new image based on this one. To create the image, we need to exit the container. So, let's do it:
[root@ce7f74b0304a /]# python3 /srv/app.py
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 123-083-749
192.168.178.60 - - [02/Apr/2020 11:08:54] "GET / HTTP/1.1" 200 -
192.168.178.60 - - [02/Apr/2020 11:08:54] "GET /favicon.ico HTTP/1.1" 404 -
[root@ce7f74b0304a /]# exit
root@devops:~#
The important lines are the last ones, where I
type Ctrl + C to stop the application, and Ctrl + D to exit the container:
We now have one container, called python_app
. If you check the last column, this is the one we wanted to create an image:
root@devops:~# docker commit python_app my_first_image
sha256:866e933c059b90a098cad06a1989d24bf16870caea1d691e2c0d3f4599f1608c
The parameter commit receives the first parameter as one container; it can be a running container or a stopped container, it does not matter; and the second parameter is the image name. Therefore, we are creating a new image called my_first_image
.
You can check the images that you have running by the following command:
We can see two images; one is the CentOS image that we downloaded from the Docker Hub, which is the official repository. The other one is my_first_image
that we created just now. You can create many instances of your application that you want just by running the following command:
root@devops:~# docker run -ti -p 5000:5000 my_first_image python3 /srv/app.py
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
The parameter -p 5000:5000
, is mapping the port 5000
from our local machine to the port 5000
of our container.
If you want to publish your image to download into any other server, you can create an account in the Docker Hub https://hub.docker.com/, and send your image there. In my case, I already have an account, so I will teach you how to send your own image to the hub:
root@devops:~# docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
The username is what you defined while creating the account. You must use the Docker login, to authenticate into your repository. The username is also used to specify where you will store that image, for example:
root@devops:~# docker tag my_first_image alissonmenezes/my_first_image
root@devops:~# docker push alissonmenezes/my_first_image
The push refers to repository [docker.io/alissonmenezes/my_first_image]
ef02c4bc0109: Pushing [===========================>] 22.67MB/41.69MB ef02c4bc0109: Pushed
Now, my image is published for the whole world. If I check my own profile on Docker Hub, I can see the following:
Figure 5.2
My image is saved and is prepared to run on any Docker installed around the world. So now, I will clean my environment, all the images and containers, and create a new container based on that image downloaded directly from the hub:
root@devops:~# docker system prune
WARNING! This will remove:
- all networks not used by at least one container
- all dangling images
- all dangling build cache
Are you sure you want to continue? [y/N] y
Deleted Containers:
06035663ec0423a479cddb0c287637626641c79c93896c6566efb802dc3ea35f
4bbab42a400fc72a339977886cde2e061c9c1dce78305d5cb56336e6f36d5965
adc48c7c0be4d422891b9e44146018c175120aab202a338f88f4a1847b50ba67
ce7f74b0304a9bc08bf0ccdd2832d6907408c0f6a9c80c56a75d3bdbf6738b62
595b42fceebc9e4f9c6e2d23d54a8ecd7eaead266ef114c953b1715d0f58a7ee
Total reclaimed space: 41.69MB
The command, docker system prune
is used to clean your environment, deleting all the stopped containers. I will run that and let's validate if the environment is cleaned:
No containers running. Now, let's delete all the images:
root@devops:~# docker image rm $(docker image ls)
Untagged: alissonmenezes/my_fist_image:latest
Untagged: alissonmenezes/my_fist_image@sha256:3c729fd3e1a595ff2bcf0937611732550ebde0c0d1945a87c01f979ca620b9fa
Untagged: my_first_image:latest
Deleted: sha256:866e933c059b90a098cad06a1989d24bf16870caea1d691e2c0d3f4599f1608c
Deleted: sha256:fbf13ca6b28b7113d750e70b57c59e2cfc90ae6b3c7436166161c92eef1dc219
Untagged: centos:latest
Checking if we still have images:
No images. Let's create a container based on the image that we pushed to the hub:
root@devops:~# docker run -ti -p 5000:5000 alissonmenezes/my_fist_image:latest python3 /srv/app.py
Unable to find image 'alissonmenezes/my_fist_image:latest' locally
latest: Pulling from alissonmenezes/my_fist_image
8a29a15cefae: Pull complete
f21402989f68: Pull complete
Digest: sha256:3c729fd3e1a595ff2bcf0937611732550ebde0c0d1945a87c01f979ca620b9fa
Status: Downloaded newer image for alissonmenezes/my_fist_image:latest
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 249-372-527
You can now see that the main objective of Docker is to create images for application purposes using an entire environment. We just created a version of CentOS with Python3 installed, and an application inside it. If you want to create a new version of your application, or update the CentoOS version, or even change the underlying OS, like migrating from CentOS to Alpine, you can do that. Do all the tests, create a new version of the image with the same name, send it to the hub, and download it in to your production/quality/development environment.
Creating Images with Dockerfile
Now that you already know all the steps of how to create an image and how to push it into the hub, we can automate these steps using the Dockerfile. This file helps you to track the modifications made into an image. When you just create a container, install everything and create an image. It is a difficult task to track everything that was installed. Therefore, if you want to create a new version for the same application, you have to track all the dependencies and everything, to ensure that all the earlier dependencies are still present in the new image.
Now, create a file, called Dockerfile, and we will include all the steps running in the container within the file:
from centos
maintainer alisson.copyleft@gmail.com
run yum clean all
run yum install python3 python3-pip -y
run python3 -m pip install flask
copy app.py /srv/app.py
expose 5000
cmd ["python3","/srv/app.py"]
Earlier, you have the file content, which has exactly all the commands that we ran within the container. The statements are self-explained, thus, you can use:
from
, to specify the base image.maintainer
, to specify who is maintaining the image.run
, will execute the commands within the container.copy
, copy one file from the local machine to the container.expose
, publishes the container port.cmd
, the command which will ensure the container running.
To create the image based on Dockerfile, run the following command:
root@devops:~# docker build . -t my_new_image
Sending build context to Docker daemon 735.7kB
Step 1/8 : from centos
latest: Pulling from library/centos
8a29a15cefae: Pulling fs layer
The Docker build will look for a Dockerfile inside the current directory. The parameter -t
means TAG
, which will define the image name. So, after the command finishes, you can just create a new container by running the following command:
Successfully built b1b23966bb89
Successfully tagged my_new_image:latest
root@devops:~# docker run my_new_image
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 130-050-890
It easy to see that now we did not have to pass any additional parameters, because all of them were already passed in the Dockerfile. The Dockerfile basically did the same steps for us before it created a new container. If you check the output, you will see the following:
Successfully built b1b23966bb89
This is the container ID, and in the end the container was tagged with the name specified in the Docker build:
Successfully tagged my_new_image:latest
You can directly put the repository name and push it there. Now, you already know the basics of Docker and it is the minimum requirement for you start to Dockerize your applications.
Conclusion
To conclude, we could see that working with Docker is an easy task. We just have to apply our previous knowledge in a different area of work. Then, you can create a version of your environment, including Infrastructure as Code, ship it to everywhere you want to, but Docker, in itself, is not enough for the large production environments. For that, we have a chapter about Kubernetes and Docker is a requirement for us to work with it.