Docker is perhaps responsible for the proliferation of containers in application development. The concept of containers is quite old, and can be traced back in the 1980s by chroot wherein different user spaces can be used within the same operating system. Once Docker was introduced however, it paved the way for further developments in containerization and changed the way how we develop and deploy software.

In this article we will discuss how to install and setup Docker in your local machine. Common Docker commands will be introduced that will equip you with foundational knowledge for tackling the next steps in application development using containers (such as Compose).

Installing Docker

The following steps are specific to most Linux distributions, but after the installation step, the concepts discussed here can be applied to any operating system.

First, we need to ensure that old versions of Docker are removed so that we can install the latest version.

sudo apt-get remove docker docker-engine docker.io containerd runc

Update your package repositories and install the requirements for Docker.

sudo apt-get update 
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common

Install Docker’s official GPG key and add the stable repository.

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"

Update the repositories again and install Docker CE, the CLI and associated packages. The CE means “Community Edition”, which is the free, open-source version of Docker. Docker (the company) also has an “Enterprise Edition” (EE) that is targeted specifically for businesses and large deployments. Both CE and EE share the same core features, but EE has more advanced management and support systems.

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

Note: If you encountered an error similar to the message below and you were not able to install Docker:

Package 'docker-ce' has no installation candidate

This means that you are running an old version of a Linux distribution, and so you will need to add the repository pointing to an older distribution.

sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic test"

Once Docker is intalled, you can now run it using the CLI.

docker version

Running this command will show you the installed versions of the Docker server and Docker client. If however, you only see the client version and not the server, this means that you do not have the correct permissions to run Docker. While you can run the commands using sudo, this is not advisable. Instead, you need to add your login to the “docker” group.

sudo usermod -aG docker $USER

After running the command, you will need to log out and log in again for the group settings to take effect. Run the docker version command again, and you should be able to see both the client and server versions.

Docker also provides an easy way to fully verify your installation by running a “hello-world” image:

docker run hello-world

This command downloads the “hello-world” Docker image, creates a container in your machine, and then runs the container. This is basically the fundamental Docker flow and if this command executes without errors, then it confirms that everything is installed properly.

It is also useful to make the Docker daemon run automatically after a reboot. To do this, add Docker to the startup programs in your machine:

sudo systemctl enable docker

Images and Containers

In Docker, we have concepts called images and containers. It is important to understand what they are and how they relate to each other.

Images are used to generate the containers. They are read-only, static files that cannot be run by themselves. You can think of it as the template or a snapshot of the containers. Images can be tagged with a version so you will know what is inside that image. In the examples in this article we will use a Ruby image, which means that you can generate containers from this image that already has Ruby installed.

Images can be shared, which is one of the reasons why Docker has proliferated. Today, you can see many services and applications that have their own Docker image. For example, there is an image for Nginx, for PostgreSQL, for Redis, and so on. By using these images, you can set up your entire application and run them basically anywhere. These images can be found in Docker Hub.

While you can create and share your own images publicly in Docker Hub, sometimes you need to share them only within your team. This is where the concept of a private repository becomes important. You can purchase a plan in Docker Hub to be able to push private images, similar to Github. Docker also provides its own Registry image that you can install yourself so you can have your very own Docker Registry. Cloud providers such as AWS and Google Cloud Platform also provide a Registry service (at a cost) where you can also host your private images.

Containers are running instances of an image. You can create as many containers as you want from a single image. Containers have an additional read-write layer that allows you to run libraries and applications. For example, in a standard Rails application, the application code and the database will both run on their own separate containers.

One fundamental concept in Docker is that containers are treated as ephemeral. This means that containers can be created, rebuilt, and destroyed at any time. It is not ideal that we keep data inside a container as this data will be lost once that container gets destroyed. The best practice is to keep your configuration and persisted data out of the container itself, so your application can run on any newly-generated container without an issue. This is achieved in Docker using external configuration files and mounted volumes.

Running Docker

Now that you have installed Docker and confirmed that it is working, let’s try to understand how it works in practice. As a Ruby developer, let’s run a simple Ruby command that prints a string as an output.

docker run --rm ruby:2.7 ruby -e "puts 'Running Ruby in Docker!'"
> Running Ruby in Docker!

Looks like we can run Ruby commands now! Breaking the command down to its components:

  • docker – This is the Docker Command Line Interface (CLI)
  • run – This is one of the most fundamental Docker commands. What it does is it creates a container from a specified image and then runs that container
  • –rm – When you create and run a container, it is persisted in your machine so you can use it again. Using this flag, when the container stops running, it also deletes it. This prevents multiple containers of the same image lying around and can save you disk space in the long run.
  • ruby:2.7 – This is actually composed of two parts: the image and the version. In our example, we will create a container based on the “ruby” image, with a version of 2.7. This will create a container that has Ruby 2.7 installed. You can refer to the available Ruby images here.
  • ruby -e “puts ‘Running Ruby in Docker!'” – This is our sample command that will be executed in the Ruby 2.7 container that we created.

This example illustrates that we can run commands even though we cannot do it natively in our local machine. In this manner, we can use Ruby even though we do not have any installation dependencies and the actual Ruby executable installed in our machine! This is an important building block that we will use to create and run an entire Rails application exclusively using Docker.

Basic Docker Commands

Docker has many useful commands and options that you will encounter as you get deeper into using this tool. However, there are some basic commands that you will encounter more often, and we will discuss each in detail.

List all images

docker images

This command will list all of the available images in your local machine. If you followed the earlier examples in this article, you will see the hello-world and the ruby:2.7 images that are now available in your local machine.

Delete an image

docker rmi {name or imageID}
docker rmi b97bae343e06
docker rmi hello-world

As time goes on, images may accumulate in your machine. Some of them may no longer be needed as they are from an older version of the image or an old project. These images consume disk space and thus it would be best to delete them.

The rmi command (remove image) will try to delete an image using an image ID or the image name. You can see the image ID or name when you list the existing images in your system. However, it will not delete an image if there is an existing container that came from that image, so you will need to remove all created containers from that image first before you can delete the image itself.

You can also delete multiple images by specifying their image IDs or names, such as:

docker rmi image-1 image-2 image-3

List all containers

docker ps

This command lists all running containers. It will show you the Container ID, the image from where it was created, and the name of the container. If you did not specify a container name when you create it, Docker will automatically generate a random name for that container, such as “furious_colden” or “sleepy_kare”.

Note that this command does not list stopped containers (or those that are not running). To show all containers, regardless of its status, use the -a flag.

docker ps -a

Delete a container

docker rm {name or containerID}
docker rm dd0c797b1973
docker rm furious_colden

To delete a container, use the rm command. Similar to how we can delete images, you can specify multiple containers to delete by using the name or container ID.

docker rm furious_colden sleepy_kare

Building an image

Images can be created and shared with anyone. These shared images can be downloaded from a Docker registry, such as Docker Hub. Some images (such as the Ruby image we used earlier) are called official images and are usually used as building blocks in creating other images. In the context of your application however, you will need to create a custom image.

docker build /path/to/Dockerfile
docker build .

You can build an image if you have a Dockerfile. A Dockerfile is essentially a set of commands that instruct Docker on how to create an image. You can specify the actual path of the Dockerfile or you can just put in the current path. The build command will run relative to where the current directory is.

You can even build an image using a remote URL:

docker build github.com/creack/docker-firefox

Tagging an image

When you build an image, you can specify a custom image name and the version (which together is called a tag). With this, you can refer to the image by name and version instead of its image ID.

This is useful if you are publishing multiple versions of your image, such as when releasing new versions of your application. This also allows for a simple rollback mechanism when you have problems with your new image.

To tag an image, use the -t flag combined with the tag information. For example:

docker build -t myapp:1.0 .

This will build an image which has the name “myapp” and a version of “1.0”. You can also omit the version and just put in the name:

 docker build -t myapp .

If you do this, Docker will put in a default version called “latest”, and your image tag will be “myapp:latest”.

What if you already built the image? Docker also supports adding the tag for an existing image:

docker tag {imageID} myapp:1.0

Running an image

From what we understand about images and containers, we know that images are read-only. So how does running an image work? In reality, the run command creates a container first using the image specified, and then runs that container and not the image itself.

docker run --rm ruby:2.7 ruby -e "puts 'Running Ruby in Docker!'"

This is the basic structure of a run command. We specify the image name (or with a specific version/tag), and the actual command. In our previous example we executed a simple Ruby script that just prints an output in the terminal.

What if we want to access the container via its own terminal? To access the container’s shell we need additional flags in the run command.

docker run -it --rm ruby:2.7 bash

Here we actually added two flags that we combined into -it:

  • -i to enable input commands to the container
  • -t to use a terminal emulator

If you do not use these flags, then you will not be able to use the container’s shell as the bash command will execute, finish the process and then terminate the container as well.

If you want to run multiple commands in a container but do not want to open a bash terminal, you can use command chaining:

docker run --rm ruby:2.7 bash -c "cd /usr/src/app && rails db:migrate"

Getting the container details

The inspect command displays more information about a container, such as where it is located in disk, the current status, and what is its IP address.

docker inspect {containerID or name}
docker inspect 7b7717677961
docker inspect myapp_app_1

The output is a JSON array that contains all of the details of a specific container.

If you use Compose, Docker provides a built in private network and DNS which means you usually will not need to access the container by its IP address. However, if this information is needed, this is available for you using this command:

docker inspect --format '{{ .NetworkSettings.IPAddress }}' myapp_app_1

You can use the format flag to get any value in the JSON response as needed, such as the current container state.

docker inspect --format '{{ .State.Status }}' myapp_app_1

Packed and ready!

Congratulations! By now you will have a good understanding on how Docker works and what images and containers are for. Armed with this basic knowledge, you are now ready to embark on further adventures in containerization, such as Compose, and eventually delve into production orchestration tools such as Kubernetes.

One thought on “Introduction to Docker

Leave a Reply

Your email address will not be published. Required fields are marked *