Docker Made Easy. Part I.
Welcome to Part I of the Docker Series where we will learn about all the basic commands which you need to master, to then containerize your docker applications. I know learning these commands might be a bit tedious, but I have tried my best to explain everything incisively. At the end of this article, you’ll also find a cheat sheet of all the docker commands which you can refer at any point in time.
In this part of the Docker Series, we will learn the following
- What is Docker and why do we even use it?
- What are Containers?
- What are Docker Images and how to create them?
- Finally, we will be creating a rudimentary NodeJS Image.
What is Docker?
Docker is a set of platform as a service products that use OS-level virtualization to deliver software in packages called containers.
Why do we even need Docker?
What is the first thing you do when you are newly appointed to an ongoing project?
Let’s see, you might have been provided with an installation setup guide for the dependencies, which you’ll need to install and configure to get all the servers up in your local. Don’t we all really hate this step?
Let me add another question. Have you ever installed all the dependencies in your first attempt?
Not sure about you, but I have failed at it every time. We get stuck with an issue, then we troubleshoot that error, we find a solution, and then re-run the installer and ta-da we now have another issue.
If you like troubleshooting try installing Redis in your system.
Redis is an in-memory data structure store, used as a database, cache, and message broker.
https://redis.io/download
Now, what if I tell you that we can bring the server up using just one command? Wouldn’t that be cool? And this is what Docker does. It helps us in bringing the server up in a fraction of seconds.
Just to give you a gist you can download Redis with the help of docker using just one command.
docker run -it redis
And yey, we have up and running instance of Redis in our system.
To understand Docker we first need to understand a few related terms, which are Image and Container.
What is a Docker Image?
In Docker, everything is based on Images. An image is a combination of a file system and parameters. It is a single file with all the dependencies and configurations required to run a program.
Image is also the only file that gets stored in our hard drive and at some point in time, we can use this image to create something called a container.
What is a Container?
A container is an instance of an image, which is a running program.
So container can also be called as a program with its own isolated set of hardware resources (a small set of memory, or space for networking & own little space of hard drive space).
Why not just use a Virtual Machine?:
- OS Support:
Virtual machines have host OS and the guest OS inside each VM. Guest OS can be any OS, like Linux or Windows, irrespective of host OS. In contrast, Docker containers host on a single physical server with a host OS, which shares among them. - Performance:
Virtual machines are more resource-intensive than Docker containers as the virtual machines need to load the entire OS to start. The lightweight architecture of Docker containers is less resource-intensive than virtual machines.
I understand it is not easy to grasp the concept of image or container, just by going through the definition. Images and Containers are the absolute backbones of how docker works and we will eventually find what these really are by proceeding towards the rest of the article.
How is an Image and a Container related?
The relationship between an Image and a Container is analogous to that of a class and an object.
When we talk about an Image, we talk about a file system. An Image will be a build-up of a file system snapshot and a startup command.
We can imagine the above screenshot as a file system of the node. Which will have a start-up command of node.exe.
When we add a Docker Image, Kernel allocates a little section of hardware for this image and copies all the file system snapshot inside it.
Now when we run the startup command, which is also provided by the image, we then create a new instance of an image for our container.
Now with this Image, we can create multiple instances of the container. But how do we actually do it? Let’s now begin with how we can actually create an Image and Container. Too much of theory, let the fun begin now.
If you want to get hands-on, I’ll recommend you install docker and run the commands to get a stronger grasp. You can download docker using this link:
https://www.docker.com/get-started.
Let’s build our first container.
To make our life easy, we already have almost all relevant official docker images ready to use. These official images are present in Docker-hub.
Now to make a container for these images. We just have to run a command:
docker run <image name>
Now if suppose I run :
docker run hello-world
(“Hello-world” is an official image that we can use from the docker hub which just returns "Hello from Docker” as an output.)
- The docker command is specific and tells the Docker program on the Operating System that something needs to be done.
- The run command is used to mention that we want to create an instance of an image, which is then called a container.
- Finally, “hello-world” represents the image from which the container is made
With the knowledge of what a container is, I want you to imagine what steps would have happened after we run this command.
- First: The file system snapshot of the hello-world image would have been added to our hardware.
- Second: Our container then runs the startup command provided by our image, prints “Hello from Docker!” & then exits.
What exactly is the “run” command?
The run command is responsible for creating and running a container from an image. docker run = docker create + docker start.
So we can create a container of an image using :
Creating a container will just add the file snapshot inside our hard drive.
docker create <image name>
When we run this command, we get the id of the container as output, we can then use that container id to start the container.
Here we give our startup command to run our container.
docker start <container id>
Just a side note: We don’t need to copy the entire container id, a few starting characters will do the job.
Why did we not see any output though? Let’s find out!
How to show the Output of a container using “start” command?
By default, the docker runs show us all the output of the container, but the docker start is the opposite.
To watch the output from the container and print it to our terminal we will have to use the below command.
“-a” simply means to attach the terminal.
docker start -a <container id>
Overriding a default command (starter command)
As said earlier all the images come up with a starter command, which runs when you run the image. But what if you want to run an alternate command which needs to be executed inside the container, and not the starter command?
Here’s how we do it:
docker run <image name> command
Now suppose we need to override the starter command of the busybox (an official docker image) image by adding any other command.docker run busybox echo Hi
This will simply override the starting command of busybox to echo command, which will then display the output as “Hi”
“ls” command list all the code inside your directory. So we can override the starter command with the ls command to see the file inside our image (You guessed it, this will be our file snapshot)
Listing all the containers
When we run multiple images we create containers for each image. So, to list out all the running containers we can use the below command:
docker ps
In this project, we have made containers using two images: hello-world & busybox.
Let’s see what our docker ps command shows:
Why are we not seeing any containers here?
One thing which we have to always keep in mind is that the purpose of a container is to only run the starter command or the overridden default command. Once the container runs this command, it exits us back to our terminal. The purpose of that container is then completed.
To see docker ps
inaction let's add a default code that will not exit immediately. For example, sleep command. This command basically tells the container to sleep for the time duration provided and once the sleep time is completed then it will exit out.
This is how we can see all the running containers. But now what if we need to see all the container, even those which have exited, we can then use the below command:
docker ps --all
This command shows us all the containers, the container which has only been created, the container which is running, and the container which has exited.
Restarting the exited container.
Once a container stops, it does not mean it is dead, we can restart the container by the start command and providing the container id.
docker start -a <container id>
How to stop a container?
Suppose we run an image by providing a default command of sleep for a thousand seconds. Due to some reason we now want to stop this container. Do we need to wait for a thousand seconds? Obviously, No. To stop a running container we use the following command:
docker stop <container id>
OR
docker kill <container id>
Now, what is the difference here? With the stop command, the container takes some time to clean up and then shuts itself. Kill command on the other hand will stop the container instantaneously without any cleanup process.
How to listen to the standard input of a container?
By default, the docker container does not listen to the standard input. What do we mean by this? Suppose you have a very basic image which when runs, asks you for your name. When we enter our name in the console. The output gets printed as “Hello ‘YourName’ ”.
Let’s name this image as greet.
Now what do you think will happen if we run docker run greet
?
We will just see the output as “Hello”.
No input prompt appears. Now to connect our container with the standard inputs. We use the “-i” flag in our run command.
We often see the “-i ”flag combined with the “-t” flag. Or just “-it” flag.
The basic use case of using “-t” is just to give our standard input the feel which we might get in an actual terminal.
docker run -i greet
OR
docker run -i -t greet
OR
docker run -it greet
How to execute a command in a running container?
Let’s take an example of the “Redis” image.
Redis is an in-memory data structure store, used as a database, cache, and message broker.
When we execute docker run redis
, we get our container to run. Now if suppose I need to connect to my redis-cli, I’ll have to write a command to get inside my running container. We can do that by using exec.
docker exec -it <container id> redis-cli
We now have access to redis-cli inside our container.
So, the main use case of the exec command is to get the terminal/shell access in our running container. By doing so we have access to all our Linux commands (cd, ls, echo, etc) which will be extremely powerful for debugging.
We can achieve this by writing “sh” at the end of our exec command.
docker exec -it <container id> sh
Since you all have reached so far here’s a meme to keep you all going:
Who does not love Michael Scott? :P. Now guessing, you haven't started watching this episode (Season 4 — Episode 10) of Office, let’s move on!
Enough on Containers, How to build an Image?
Throughout this article we have learned a lot about containers, now it’s time to focus on Images. Till now we have only used Images that were pre-built by other developers. To create our own image we will need to create a Dockerfile.
What is a Dockerfile?
Dockerfile is simply a text file written in a specific format, which basically has instructions & arguments that Docker can understand.
The instructions are written on the left-hand side (in CAPS). Everything to the right-hand side is what we call an argument.
FROM node
Or
RUN npm install
Here FROM & RUN are our instruction and “node” & “npm install” are our arguments.
How to write a Dockerfile?
As mentioned earlier Dockerfile is used to build our image. One thing that we need to understand here is that:
“Every Docker Image must be based off from another Image”. It can be based either on an Operating System or an exiting Image.
For example:
FROM ubuntu // Based off from an OSFrom node // Based off from an exiting image
So our Dockerfile must start with a FROM command.
What exactly does this FROM instruction do?
Suppose we have a Node application for which we need to create an Image. Here the first thing that we have to do is get the node in our system. Installing node will give access to all the node commands which we can use in our Dockerfile (for example npm install), the same if you install Ubuntu, you will get access to all the ubuntu commands (for example apt-get update).
The RUN command
The RUN command instructs the docker to run a particular command on the image which we are creating. We get access to the image commands or OS commands with the help of FROM command. To now execute these commands we need the RUN command.
A quick note, npm install will only work if you have a package.json file.
FROM ubuntuRUN apt-get update
RUN apt-get install node
RUN npm install
Here we build our Image with the help of Ubuntu OS and then with the help of apt-get command we install node, to then get access to npm i command.
Another way to do this is by actually pulling in the Node Image in our Dockerfile.
FROM node
RUN npm i
The CMD command
The CMD command adds the starter command to our Image. So when we run our Image using docker run <Image name>
It will tell our container to run the CMD command as a primary/starting command.
FROM node
RUN npm i
CMD npm start
CMD command is also sometimes written inside an array, and each element inside an array represents a specific commandCMD [“npm” , “start”]
How to build your DockerImage?
FROM, RUN, and CMD are the main commands inside a Dockerfile. Now to convert this file into an image we have to run a final command in our Docker CLI.
The “.” in the build command is what we call the build context.
docker run build .
Let’s now see everything in Action.
Inside a new-folder add a Dockerfile and a package.json file. Your package.json should look something like this:
Now open your folder directory in a CLI and run the build command.
Please ignore the warning messages for now.
The built number highlighted here is the id our newly created image.
This is it, we have now learned how to create a very basic image.
If, suppose you have missed your build number here, you can get the ids or all the images and their respective data using the below command:
docker images
We will learn about the tag in the later part of this Docker Series.
To delete an image please use the below command
docker rmi <Image id>
Conclusion:
This is it for Part I of this docker series. I completely understand that all this information is too much to take at once, but I suggest installing docker in your system and trying all the commands which we have discussed, this will help you get an insight of the docker functionality lucidly.
Here’s a cheat sheet for all the docker commands:
In the second part, we will discuss much more on Images on how to containerize a React Application.
If you have anything which is up in the air for you regarding the basics of Containers & Images please add it in a comment and I’ll try to answer your queries. Thank you.