Getting Started with Docker: A Tutorial

Docker containers have taken the software industry by storm. Ever since its launch in 2013, Docker’s usage and popularity have grown at a rapid pace. 

Docker has saved organizations from the challenges of managing dependency and version conflicts across multiple environments by providing a portable, secure, and (most importantly) reliable container technology for shipping applications. This has saved teams time and resources otherwise wasted in trivial tasks like manually configuring and managing development and production environments or resolving dependency conflicts between different services for running applications. As a result, Docker has become an essential tool in a developer’s toolkit and is, therefore, a must-know when applying for jobs in the software industry.

undefined

Docker -- Interest over time (Source: Google Trends)

However, by nature, the technology at first might seem a little arcane for beginners. Being comfortable with Docker and its components requires a good understanding of containerization, operating systems, networking, and other concepts. Understanding them and their interplay with Docker’s API can therefore seem challenging at first.

In this post, we are going to start from scratch. Join us even if you are new to Docker. From its installation to a quick overview of its essential components to covering all the basic commands, running existing images, and building your own ones for serving web pages, we will cover everything. Try running the Docker commands on your system as you go along the post to build intuition about how things work. And consider this post as a beginner’s guide that you can keep coming back to brush up on your understanding of Docker’s basics.

Here’s an outline of what we’ll be covering so you can easily navigate or skip ahead in the guide -

{Step #1} Installation 

{Step #2} Brief Overview of Docker’s Architecture and Components 

{Step #3} Docker Basics 

{Step #4} Working with Images

{Step #5} Working with Containers

{Step #6} Custom Container Example: Serving HTML pages

{Step #7} Create Your Own Image

Try it For Yourself 

{Step #1} Installation 

Let’s start by installing Docker. You can download the installer or the package from the website here. Based on your OS and your distribution, you can check the system requirements and follow the relatively straightforward steps. Below are the links to the installation guides for the most common OSes.

undefined

Source: Docker Docs

You can ensure that the installation has been successful by running docker -v  in your terminal or command prompt. 

undefined

Output

Installing Docker is usually uncomplicated and effortless, but there are excellent places to get help from the enormous active developer community if you still face any issues.

Once installed, you can get system-wide information about your installation using the docker info command. This includes information like the client and server versions, driver information, security information, runtimes, etc.

docker info

undefined

Output

{Step #2} Brief Overview of Docker’s Architecture and Components 

If you are well versed with the fundamentals of Docker containers and want to jump to the hands-on stuff, you can skip this section and navigate to Step 3 (Container Basics). But if you are a beginner and want to get a sense of what Docker helps with, what it is capable of, and how it works internally, let’s do a quick overview to be all on the same page. Below are some of the most common Docker keywords that you are likely to have encountered when working with Docker.

Docker Container: A container is an isolated, self-contained run-time environment with its own set of services, processes, network interfaces, and dependencies. It’s a lightweight, portable, secure, independent unit of software that is much more reliable, easy to distribute, set up, and deploy. These containers are isolated from each other and the host (the machine with Docker installed). Still, they can effectively share the underlying kernel and resources (as shown below).

undefined

Source: What is a Docker Container

Docker containers are runnable instances built from templates with all the requisite information about the service. We’ll get to that in a while.

Docker Engine: The engine refers to the underlying containerization software that interacts with your operating system’s kernel to create and manage multiple containers and share resources. The engine encompasses a client-server architecture that we discuss next.

Docker Daemon (Server): The daemon is the core of the Docker Engine. It is the server that receives the commands from the client for building and managing images and containers. 

undefined

Source: Docker Overview

Docker CLI (Client): The command-line interface allows us to give commands to the Docker daemon (internally through a REST API) to build images, manage containers, networking, data volumes, etc. This client can either be on the same host or even connect through a remote instance.

Docker Image: Images are essentially the templates used for building containers. They contain all the information about the service, process, or application that you wish to containerize. This includes the code, dependencies, commands, and other configuration information. Docker containers are the instantiations of images, i.e., you can make multiple containers from one image template (as shown below).

Source: Docker Overview

Docker Hub: This is the repository of hundreds of thousands of pre-built Docker images offered by the community for the community. It provides a platform for you to host your or your organizations’ images and share them with others to pull (download) and use.

For example, suppose I want my container to run an Ubuntu OS distribution. In that case, I can easily pull the Ubuntu image from the hub by using an inbuilt docker pull command that we’ll explore in more detail later. This library of public and private images is available at hub.docker.com.

Dockerfile: You can also create your custom Docker images (and consequently your containers) using YAML-based, easy-to-read configuration files known as Dockerfiles. These files contain information about your container’s OS, dependencies, environment variables, commands to run, networking configurations, etc., and can build your customized images (using the docker build command). 

Source: Docker Blog



Here’s an example of a basic Dockerfile --

FROM Ubuntu

RUN apt-get update
RUN apt-get install python
RUN python3 -m pip install flask

COPY . /opt/my-code
ENTRYPOINT FLASK_APP=/opt/my-code/app.py flask run

That’s it for a quick overview and should be enough to help you get a headstart in Docker and navigate the rest of the post with some hands-on examples.

However, if you would like to get more in-depth information about Docker and containerization works under the hood, feel free to check out the What is a Docker Container post on our blog.

{Step #3} Docker Basics 

Now let’s get our hands dirty and run some actual containers. We’ll start with the hello world application of the Docker-verse. This is equivalent to running a “hello world” container (offered by Docker itself) on our system. This is also a good way of ensuring that Docker’s installation has been successful and that everything’s working as expected.

Run a Container 

docker run hello-world

undefined

Output

As you can see, Docks pulls the image from the internet, runs the container, spits out some text, and exits. Let’s breakdown the steps that took place internally to achieve this --

  1. Our CLI client sends the command to run the image to the Docker daemon (server).
  2. Because the ‘hello-world’ image wasn’t present on our system, it was pulled (downloaded) from the Docker Hub.
  3. The pulled image was then used to create a new container and run it. This printed some content that was streamed to our CLI client, i.e., to our terminal.

Before we work with more useful containers or create some of our own, let’s explore the API around just this dummy one and get some more context.

List Containers 

docker ps or docker container ls

Output


This command lists all the running containers on your system.

Output

This returns an empty list because the container we ran has exited. To view all the containers (including those previously running), we need to add the -a or --all option.

docker ps --all or docker container ls --all

Output

As you can see, our container, named gallant_chebyshev, gets an ID when instantiated and exits in two seconds. Now let’s rerun the container.

docker run hello-world

Output

As you can see, this time, there’s no information about downloading the image; you only need to download each image once and reuse it as many times as you like. Just for a sanity check, let’s list all of our containers again.

docker ps --all

Output

As expected, we now have a new container with a different name (vibrant_hawking) and ID.

{Step #4} Working with Images 

Now let’s look at Docker images and try to build an intuition of the commands we can use to pull, instantiate, list, inspect, and remove images.

Pull an Image 

To run any container of your choice, you can pull the corresponding image from Docker Hub by navigating to the required one, copying its identifier, and using the pull command. Let’s say we were to use this fun image -- docker/whalesay.

Docker Hub: docker/whale-say

We can use the pull command provided on the page itself as such:

docker pull docker/whalesay 

Output

Run a Container 

Now let’s run the container using the demo command provided on the whalesay image’s page.

docker run cowsay boo

Output

This container takes in a string as an argument and prints a pretty little Docker whale with the message. This was just to showcase the steps taken internally when you run a container. You could also use docker run to directly pull and run the container, as we did with the hello-world image.

List all Images 

Now that we have worked with two images, let us list all the images pulled on your system using the docker images or the docker image ls command.

docker image ls

Output

This provides us basic information about the images like the repository they belong to, tags (versions), IDs, relative creation date, and size. 

Inspect an Image 

To get more low-level information about these images (or any Docker object for that matter), we can use the docker inspect command along with the object’s name or ID.

docker inspect docker/whalesay

Output

This provides us JSON-formatted information that includes the date of creation, the container’s configuration, architecture, operating system, working directory, volumes, Docker version, and so much more. You can also filter out this profuse output by using the --format option along with the data points you are interested in as such --

docker inspect --format='{{.Os}}/{{.Architecture}}' docker/whalesay

This filters out the container’s OS and architecture in the provided formatting.

Output

Remove an Image 

The list command (previously) showed us that the docker/whalesay image, for some reason, was 247 MBs in size, as opposed to the tiny hello-world image (13 KBs). This seems unnecessarily sizable for creating a container that barely prints a string. Let’s see how we can get rid of it.

docker image rm docker/whalesay 

Output

This, however, fails because the container we ran using this image still exists. We can either remove the corresponding container first using docker container rm (we’ll explore this more in the next section) or forcefully remove the image using the -f flag. Let’s do things the proper way and remove the container first. We’ll fetch the container ID by listing all the containers and then use that to remove the container.

docker container ls -a

Output

docker container rm <container-id>

Output

Now that no container is using this image we wish to remove, we can safely proceed.

docker image rm docker/whalesay 

Output

We can further confirm that the image was removed by listing all images.

Output

Those were some of the most common image-related commands in Docker (except for building images - which we’ll get into a little later in the post). To get more information about any command you come across - about what it does, how to use it, the options it accepts, etc., (and this applies to most CLI applications and commands), you can use the --help or -h option as such --

docker image --help

Output

{Step #5} Working with Containers 

Before we go through the most common Docker commands for containers, we’ll take a look at a container’s lifecycle and the different states in it. We’ll then dive into the commands that allow us to manipulate our containers as we like.

Docker Container Lifecycle

Source: DevOps School

The container’s creation happens first -- it is when the container is created from the image before it runs. Once the container is up and running, it can be moved to three states - paused, stopped, or killed.

Paused: Pausing suspends all the processes running in the containers using the SIGSTOP signal. This frees the CPU for other tasks, but the container retains its share of the memory for when the container needs to be resumed (unpaused).

Stopped: This stops the container processes by first sending a SIGTERM signal and then the SIGKILL signal after a grace period (to allow the code some time to save the progress).

Killed: The kill command is a little less subtle in this regard and abruptly kills the container’s main process. 

The above diagram presents a good picture of the different container states and how to switch between them. Now let’s explore this lifecycle using Docker commands.

Create a Container 

The first step in the life cycle is to create a container from an image. Let’s use the hello-world image that we already have pulled on our machine, like so --

docker create hello-world

Output

Note: You can also give your container a name by using the --name option as such --

docker create --name my_container hello-world

docker ps --all

Output

Upon listing the containers, the status shows that our container has been created (though not yet running).

Start a Container 

To get a container in the running state, we need to use the start command (with the container ID) after its creation.

docker start <container-id>

Output

But did this even run our container? We can’t see the usual text output.

This is because even though our container ran, its output wasn’t streamed to our client through STDOUT.  To do so, we need to use the --attach (or -a) flag. Now, if we run the same command, we should see the usual statements printed.

docker start --attach hello-world

Output

That looks more like it.

Run a Container 

Fun fact: the docker run command that we saw earlier implicitly pulls, creates, and starts our container from the image. So this time, let’s use a different image that doesn’t exit almost immediately. The hello-world container’s output suggested a more ambitious container example with an Ubuntu image. Let’s try that one. 

docker run ubuntu

Output

As you can imagine, this container also ran in the background and exited. But we’d like to interact with this Ubuntu container. So we’ll use the -i (or --interactive) and -t (or --tty) flags to run the container with access to an interactive shell as such --

docker run -i -t ubuntu

Output

Note: You can also use an -it flag to club the -i and -t flags.

Now we have an active container up and running. Let’s open another terminal tab and check this in the list of containers.

Output

Display Container Stats 

When running multiple such containers, it would be helpful to visualize in real-time their resource utilization, such as memory usage, CPU consumption, etc. This is what the stats command is for.

docker stats --all

Output

Instead of getting all your containers' stats (using --all), you can also print out just those of the ones you are interested in using their ID.

Additionally, you can get information about the processes running in containers using the docker top command.

docker top <container-id>

Output

This shows us the single running process (root) -- the single interactive shell running in the other tab.

Pause or Unpause a Container 

We looked at temporarily pausing and resuming containers in the container lifecycle earlier. Let’s try that here. The corresponding commands are pretty straightforward.

docker pause <container-id>

Output

We can see that the container has successfully paused. You can also check this yourself using the interactive shell while the container pauses (it won’t take in any commands until you unpause).

docker unpause <container-id>

Output

This should resume your container. Interestingly, all the key inputs you would have tried in the interactive shell when paused would now appear and take effect.

Stop a Container 

docker stop <container-id>

We discussed above how stopping sends an intermediate SIGTERM signal for a grace period before killing the process using SIGKILL.

Kill a Container 

docker kill <container-id>

Output

On the other hand, the kill command abruptly kills your container process at one go.

Remove a Container 

Now let’s remove the killed container from our system altogether.

docker rm <container-id>

Output

Enough with basic commands. Let’s create a tiny container application that utilizes our local filesystem and serves some HTML web pages. 

{Step #6} Custom Container Example: Serving HTML pages 

Nginx is a popular open-source web server available as a Docker image on Docker Hub. We’ll use this image to create a web server container that can serve some static HTML files on our system.

First, we’ll create a sample HTML web page index.html in a folder named “site-content” anywhere on our system.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Docker Nginx</title>
</head>
<body>
  <h2>Hello world!</h2>
  <p> - Nginx Docker container </p>
</body>
</html>

Now we’ll use the docker run command with a few fancy options for ensuring that the server knows where to pick the files from and where to direct them. Don’t worry; we’ll decompose the command to understand better what’s happening here. This way, we’ll also explore some other options that we might not have had a chance to discuss above.

docker run -it --rm -d -p 8080:80 --name my_server -v ~/path/to/site-content/folder:/usr/share/nginx/html nginx

Output

We can now access our web page on our localhost, port 8080 (https://127.0.0.1:8080). 

Output

Let’s list the running containers to display the status and port information and ensure that things are running smoothly.

Output

Once done, you can stop your container using --

docker stop my_server

Output

This way of mounting the local filesystem can be pretty helpful for serving files through a running container. However, if we were to move these around between different environments, it might be much more portable and convenient to have the web pages packaged into the container itself.   

{Step #7} Create Your Own Image 

We used bind mounts previously to utilize the HTML files on our system. Now let us create an image of our own that packages the server and the HTML files. To keep things simple, we’ll recreate the previous example but using a Dockerfile this time. 

As we saw previously, a Dockerfile is a file that contains all the information required to build an image and the container.

Creating a Dockerfile 

Let’s enter the same ‘site-content’ directory and create a file named Dockerfile.

FROM nginx
COPY ./index.html /usr/share/nginx/html/index.html

The first line here refers to the base image we wish to use for our container, i.e., Nginx. All Dockerfiles start with a base image, on top of which the rest of the commands execute.

The COPY statement is pretty straightforward. As discussed in the previous example, Nginx expects the server files to exist in the specified folder. This statement does precisely that by copying the index.html file to the required location.

This two-line file is all we need now to build our image. 

Building an Image 

Now from within that same directory, we’ll use the following command to build an image using the Dockerfile --

docker build -t my_server .

The -t option in the command creates an identifier tag for our image that we’ll use to create containers out of it. And, the “ . ” (dot) indicates the current directory as the entry-point path.

Output

As you can see, our image “my_server:latest” is built successfully. We now don't need the index.html file anymore. This image, in itself, has everything we might ever need to run our dummy web app.

Running a Container 

Now we can run the same command as before with our new image here without using bind mounts this time.

docker run -it --rm -d -p 8080:80 --name my_nginx_server  my_server

Output

And this works as expected, same as before -- but this time around, the image is much more portable and completely self-sufficient in itself. You can ship this to wherever you like, and your web page would work just fine.

Output

Try it For Yourself 

Now that you’ve gained a good understanding of containers, images, Docker, and its API, go ahead and create your own containers. Think about some of your applications that could benefit from the virtues of containerization, and go for it!

If you are interested in learning more about Docker, check out the following posts from our blog:

And if you are motivated to understand your applications better and boost their performance, check out ScoutAPM and get started with a 14-day free trial (no credit card needed).