This blog post is a continuation of this blog post where I attempted to give an introduction of Microservices and Docker containers. In this post we are going to delve deeper into Docker containers and get hands-on practice which should give a better understanding.
In this blog post we are going to touch on the following:
A Docker image is the application binaries and dependencies that include Metadata about the image data and how to run the images that build a Docker container. An image is a file that also acts as the starting point when using Docker. An image does not have a complete OS, there is no Kernel, no drivers
Enough theory let us get started with our first image. It will be good if I point out at this point that we are not exactly creating our own image from scratch, but we will get to that soon. What we are going to do now is launch a container using an image. So, assuming you have Docker setup already; run the following below:
docker run debian echo "Hello World,I am just getting started.Hurray"
Depending on your internet connection the output might take a while, so do not fret. The image below is the expected output which might differ depending on your OS. However, you should get the output “Hello World, I am just getting started. Hurray” if everything has been set up correctly.
Let me explain what is happening from the command we just ran and the output we just got.
In summary what just happened is:
It will interest you to know that docker has a caching system such that if you run the previous command again it will immediately launch the container without downloading. The caching system also comes in handy in scenarios where you are downloading a heavy image and due to network issues, the download is interrupted, layers of that image that are already saved into your machine won’t be downloaded when you resume the download again thus saving time.
Running docker images –a will give you the list of images which you have in your machine.
That was a simple container, let’s go further to create another container which is relatively complex than the previous ones. This new container will help us see the importance of Dockerfiles.
Run the following command:
docker run -it --name cowsay --hostname cowsay debian bash
-it tells docker we want an interactive session
— name allows us to give a name to the container.
— hostname allows us to pass a hostname to the container
bash gives us a bash shell.
Once the container has been launched as shown in the image above, we install some packages. Using the commands below:
apt-get update apt-get install -y cowsay fortune
Then run the following command as shown below.
usr/games/fortune | /usr/games/cowsay
You should be getting the outputs shown in the image below after running the above commands.
It will be good to keep this container, to turn it into an image we use the docker commit command and this can be done irrespective of the state of the container. We turn the container into an image as listed below.
Let's exist the container before we turn it into an image by simply typing exit
Run the following command:
docker commit cowsay first/coswayimage
The output you should be getting should be like that seen in the image below. The value returned is the unique id of the image.
Now, we can run our image that has cowsay installed with the following command.
docker run first/cowsayimage /usr/games/cowsay “This is quite easy”
You should get the following input.
It is great we have our own image. However, what happens if we need to change something, well with our present setup we would have to manually repeat our steps from that point. For example, say we need to change the base image (debian).
This is where a Dockerfile comes in. It helps us to build an automated build for the image.
A Dockerfile is just a text file that contains a set of instructions that can be used to create a Docker image.
To create a Dockerfile for our cowsay application let us create a new folder and a file inside that folder.
Create a new folder using:
mkdir cowsay, then we change into that directory using cd cowsay. When in that folder, we create a new file using touch Dockerfile. We then go ahead to insert the following contents into the Dockerfile using nano Dockerfile.
FROM Debian:latest RUN apt-get update && apt-get install -y cowsay fortune
The FROM instructions specifies the base image to use, and in this case, we are still using debian as before. However, I am explicitly specifying the version of debian I want. However, by default if I do not specify the version I want, the latest version is automatically downloaded. So, if you want to experiment with another version, you can go ahead.
One thing that this our particular Dockerfile has with other Dockerfiles you will create or has been created is that; Dockerfile must have a FROM instruction as the first Non comment instruction.
The RUN instruction simply specifies a shell command to execute inside the image, which in our case is installing cowsay and fortune like we did previously.
After completing the above we are now set to build the image by running docker build command inside the cowsay directory. To check if you are in the directory run ls, you should be getting Dockerfile returned
Now run this command.
docker build –t first/cowsay-Dockerfile .
N.b: Do not leave the last dot after the command, it is not a mistake. This provides us with the build context. If I remember correctly my first time building an image, I kept getting an error because I left that part out.
When you run the above command, the result should be like what is displayed in the image below.
When our image is all set, we can run it just as we did before:
docker run first/cowsay-Dockerfile /usr/games/cowsay "Using Dockerfiles gives us a more organised way to maintain our images."
At this point we are doing quite well. However, do we really want the user to type out such a long command? Well personally I wouldn’t want to. To avoid this, we make use of the ENTRYPOINT Dockerfile instruction. The ENTRYPOINT instruction lets us specify an executable whose function is to handle any arguments passed to docker run.
Going back to our Dockerfile, insert the following line to the bottom.
Since we have made changes to the Dockerfile, to make these changes reflect in our image we need to rebuild the image using the command below like before.
docker build –t first/cowsay-Dockerfile .
Once the build is complete, we can go ahead to run the image without passing “usr/games/cowsay” and confirm the change to the Dockerfile has been implemented.
docker run first/cowsay-Dockerfile "And it keeps getting simpler and simpler"
One thing seems to be different, since using the Dockerfile we have had to pass in what we want our super cow to say, and this is quite different from the first instance when we ran our cowsay application without a Dockerfile. In this present scenario we have lost the ability to use the fortune command inside the container as input to cowsay i.e. we can't generate random sayings. To fix this we provide our own script for the ENTRYPOINT. We create a file in the same directory, we call this file entrypoint.sh and populate it with the commands below.
#!/bin/bash if [ $# -eq 0 ]; then /usr/games/fortune | /usr/games/cowsay else /usr/games/cowsay "$@" fi
Then run the following command.:
chmod +x entrypoint.sh
All this script is doing is to pipe input from fortune into cowsay when cowsay is called with no arguments; otherwise, it calls cowsay with the given arguments. To implement this what we need to do is to modify the Dockerfile to add the script into the image and call it with the ENTRYPOINT instruction. Our Dockerfile should be edited as shown below.
FROM debian RUN apt-get update && apt-get install -y cowsay fortune COPY entrypoint.sh / ENTRYPOINT ["/entrypoint.sh"]
If you have been paying close attention you will notice a new command COPY what the COPY instruction does is to simply copy a file from the host (your PC in most cases) into the image’s filesystem and it is in this format:
COPY file on the host destination path
Images, Containers, and the Union File System.
I trust you have been finding this exciting so far. However, if you are like me, I am sure you must have been wondering what the underlying process is that converts an image into a container. I will not go into all the nitty gritty details of that because trust me you do not need that. What I will be doing in this section is to give a surface explanation of the relationship between an image and a container which I believe will be sufficient to understand this complex relationship. To explain the relationship between an image and a container we need to talk about an important technology that explains Docker. This technology is called the Union File System.
The Union file system allows for multiple file systems to be overlaid and thus appear to the user as a single file system. There are several UFS implementations supported by Docker, some of them are AUFS, Overlay, BTRFS and ZFS. By running the command docker info you can see the implementation used, the implementation used is system dependent.
Docker images are made up of multiple layers each of these layers being a read-only file these layers in the docker image corresponds to each instruction in a Dockerfile. In other words, a layer is created for each instruction in a Dockerfile and sits on top of the previous layer. When an image is converted into a container like shown in several examples above either using the docker run or docker create command the Docker engine adds a read-write filesystem on top of the read only filesystems in the image and asides adding that the Docker engine initializes various settings such as the IP address, name, ID, and resource limits.
Our cowsay application might not be the most useful app in the world but it is dear to us, and we might like to share this with other people. Rember our first image i.e. the debian Image. We got it from the Docker hub when we initially ran the “FROM debian” command. Also, we can upload our own images to the Docker hub for others to download and use.
To upload our image into the Docker hub we need to sign up for an account. After signing up all you need do is to tag the image into an appropriately named repository and then use the docker push command to upload it to the docker hub.
One more thing just for fun let us add a MAINTAINER instruction to the Dockerfile and all we are doing is just adding our contact info
FROM debian MAINTAINER Your Name <email@example.com> RUN apt-get update && apt-get install -y cowsay fortune COPY entrypoint.sh / ENTRYPOINT ["/entrypoint.sh"]
Now we would rebuild the image and upload it to Docker Hub. However, the repository in this scenario will begin with your username on the Docker Hub. So, for me that will be Adediwura followed by / and any name of your choosing for the image.
docker build –t Adediwura/cowsay. docker push Adediwura/cowsay
By default, the image is given a tag of “latest” if no tag is given. To specify a tag just add it after the repository name with a colon like so.
docker build –t Adediwura/cowsay:v1 .
Once the push is complete anyone around the world can download your image using the below docker command:
docker pull Adediwura/cowsay
Thank you for completing this blog post. If you do find it educative. I would appreciate you dropping a comment and subscribing to get updated about new blog posts from me. Cheers to the New Year.