Docker Basics: A Practical Guide for Beginners

Docker Basics: A Practical Guide for Beginners

This tutorial covers the basics of getting started with Docker. We will cover

  • What Docker is and its key concepts
  • How to create a Docker image using a Dockerfile
  • How to run a container
  • Pushing an Image to Docker Hub

What Is Docker?

Docker is a technology that allows applications and their dependencies to be packaged into containers. Docker containers ensure that code will function the same, regardless of the environment they are running in. You can run your app in a Docker container on your laptop and it will behave in exactly the same way on another developer's computer, or in the cloud.

If you are new to the topic of containers, you can get up to speed by reading my previous blog post Containers and Containerization.

Understanding Key Docker Concepts

Before we start running any Docker commands, it's important to get familiar with the terminology associated with Docker.

Image - A read-only template that has a set of instructions for creating a container. It contains the source code of the application, it's libraries, dependencies, and anything else that application needs to run. Images are made up of different layers. The layers of a container image are all immutable. This means that once an image has been generated, the layers cannot be changed.

Container - A container is a running instance of an image. Containers are isolated from the host machine, as well as from each other. They share the host's operating system, CPU, and memory. One computer can run multiple containers.

container1.png FIGURE 1 - Overview of Containers

Docker Daemon - It is a service that runs on the host and is responsible for building, running, and distributing the Docker containers. The Docker daemon receives requests and returns responses from the Docker client using the HTTP protocol.

Docker command-line interface (CLI) - It is used to operate or control the Docker Daemon. When you run Docker commands, the CLI sends them to the Docker API, and the Docker Engine does the work.

Docker Engine - It is composed of the daemon process, a REST API, which is used to talk to the daemon process and instruct it what to do, and the command-line interface (CLI).

Docker REST API - The Docker daemon exposes a REST API, which the Docker client uses to interact with the Docker daemon. The API allows us to control every aspect of Docker.

Docker Under the Hood

Docker is written in the Go programming language and takes advantage of several features of the Linux kernel to deliver its functionality, namely control groups, namespaces and the union file system.

dockerinternals.png FIGURE 2 - Overview of Docker’s architecture

A control group (cgroup) is a Linux kernel feature that manages and monitors resource allocation for a given process and it sets resource limits, like CPU, memory, and network limits. Control Groups ensure that we never have a situation where a single container consumes most or all the available resources on the Docker host.

Namespaces are about isolation. When you run a container, Docker creates a set of namespaces for that container. Each aspect of a container runs in a separate namespace and its access is limited to that namespace.

Docker uses the union file system to create and layer Docker images. It is used to avoid duplicating a complete set of files each time you run an image as a new container.

Now that we are familiar with Docker terminology and understand how Docker works, it's time to get hands-on.

Installation

The installation procedures that are used to install Docker vary between platforms. Refer to https://docs.docker.com/get-docker/ and choose the installation path that is right for your machine.

If you are using Windows 10, there are separate instructions for installing Docker on Home vs. the Pro/Enterprise versions. On Windows 10 Home edition, you need WSL2 (the Windows integrated Linux kernel) installed before you install Docker.

Docker is free to use unless you are a large company of more than 250 employees or have more than $10 million in annual revenue, which requires users to have a paid Docker subscription.

Download the installer and run it, accepting all the defaults. When Docker Desktop is running you’ll see Docker’s whale icon in the taskbar near the clock.

To make sure that Docker is installed correctly on your system and is ready to accept commands, open a new Terminal window and type in the following:

docker version

This should return something similar to the following output:

Screenshot (717).png

Running Hello World in a Container

Let’s get started with Docker the same way we would with any new programming concept: running Hello World. We are going to send a command to Docker, telling it to run a container that prints out "Hello from Docker!" text to the Terminal.

docker run hello-world

The output should look similar to this:

Screenshot (718).png

What's happening here is that we’ve called the docker run command, which is responsible for launching containers.

The argument hello-world is the name of the image we are running.

Once the image has been downloaded, Docker then turns the image into a running container. The process will then exit and our container stops.

Sample Script

To demonstrate Docker in action, I'm going to use a simple Node.js script that delivers streaming video, so if you want don't have an app of your own and want to follow along with my example, make sure you have Node installed on your system. First create a new project folder and navigate into it.

mkdir docker-basics && cd docker-basics

Initialize a new npm project by running:

npm init -y

The -y argument means that we don’t have to answer any interactive questions while initializing our project.

The command will generate a package.json and package-lock.json, which are files that track the dependencies and metadata for our project.

In order for our app to stream video, we need to make an HTTP server. To do this we need to install Express.js which is the standard framework for building HTTP servers on Node.

npm i --save express

Copy the code block below into a file and name it app.js.

const express = require("express");
const fs = require("fs");

const app = express();

// Define the HTTP route for streaming video
app.get("/video", (req, res) => {

  // The path to the video file we will stream
  const path = "video.mp4";
  fs.stat(path, (err, stats) => {
    // Handle errors
    if (err) {
      console.error("An error occurred ");
      res.sendStatus(500);
      return;
    }
    // Send response to web browser
    res.writeHead(200, {
      "Content-Length": stats.size,
      "Content-Type": "video/mp4",
    });
    // Stream the video to the web browser 
    fs.createReadStream(path).pipe(res);
  });
});

// Start HTTP server on Port 3000
app.listen(3000, () => {
    console.log(`Sample app listening on port 3000, point your browser to http://localhost:3000/video`);
});

Now add any video file of your choosing to the directory.

In the terminal run the following command to start the application:

node app.js

Our server delivers streaming video to the web browser via port 3000 and the route /video, so we can watch the video directly through our browser by going to localhost:3000/video

Screenshot (711).png

Figure 3 - Watching the streaming video in a browser

To end the process inside your terminal press Ctrl-C.

Now that we have a basic Node.js app setup, we can write our Dockerfile.

Creating a Dockerfile

A Dockerfile is a text file used to create Docker container images. You can think of a Dockerfile as a series of commands that get sent to the Docker engine, and the Docker engine starts at the top and reads through those commands, executing each in turn, and building the image.

By convention, the instructions (FROM, COPY and CMD) are uppercase and the arguments are lowercase.

Copy the code below and save it in a file named Dockerfile.

FROM node:14.2.0-alpine3.11
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]

Let's look at what this Dockerfile does, step by step:

FROM

FROM node:14.2.0-alpine3.11

When you build images you start out with a base image. The FROM command tells Docker which base you would like to use for your image. We have a Node application so we're going to use a Node base image and then we're going to build on top of it.

14.2.0 is the tag of the Node image we want to use. Docker images are always referenced by tags, and when you don’t specify a tag the default, :latest tag is used. The first time you use the image, Docker will download it to your local machine and then cache it for further use.

Some official images like Node, OpenJDK and Golang provide :alpine tags. Alpine is a Linux distribution that is designed to be small and secure. It is good to have small images because they will generally be faster to build, push and pull.

We can also use multi-stage builds to reduce the size of our images. We get a multi-stage build by adding another FROM line in our Dockerfile. You can read more about multi-stage builds here.

You could also use tools like Docker Slim to achieve smaller image sizes.

WORKDIR

WORKDIR /app

The working directory command tells docker to create a directory called /app and use that as our working directory. It determines which directory you’re in by default when you start up your container from your built image. All subsequent commands will be run inside this directory.

COPY

COPY package.json ./

Here we copy our package.json into our current working directory /app, denoted by the './'.

When you build an image, the directory containing the Dockerfile is used as the context for the build. So Docker will expect to find a folder called package.json inside the context directory.

RUN

RUN npm install

Everything after the RUN command, Docker will execute. Docker will run npm install inside the image (npm is the Node package manager. It will look inside package.json for a list of dependencies and download those into the image). In our case, npm will just install Express.js since this is our project's only dependency.

COPY

COPY . ./

After the Node.js dependencies are installed, we copy the rest of the application files from our local current directory, denoted by the first dot, into the /app folder of the image, denoted by the second dot.

EXPOSE

EXPOSE 3000

The EXPOSE command indicates the port on which our container will listen for connections.

CMD

CMD ["node", "app.js"]

This command is used to give the Docker engine a default command that it should run when it starts our container.


Further commands Since this Dockerfile is specific to our streaming app, not all of the commands that are available were used. You can have a read through the Dockerfile reference in the official Docker documentation to go over all of the available commands.


On your local machine, in the same directory that contains the Dockerfile, you can have a .dockerignore file. It is similar to .gitignore file in that it allows you to list out what folders you don't want included in your image. This will ensure that we have a small Docker image that doesn't contain unnecessary files.

At this point your project structure should resemble the following:

Screenshot (716).png

Building a Docker Image

To build an image, run the following command:

docker build -t docker-intro:1.0.0 .

The docker build command builds a new image based on the instructions specified Dockerfile. Take note that there is a '.' at the end of the command which specifies the path to the Dockerfile, which in this case is the current folder. The option -t refers to a name of (or tag of) the image. You can name the image anything you like. In this case, the name is docker-intro:1.0.0. It is a unique identifier for the image in your local image cache and in image registries. The tag is how you'll refer to the image when you run containers.

The output of the build command will look similar to this:

build.png

You can see that each step printed in the output while building the container corresponds to a line in the Dockerfile. Each instruction is executed as a separate step that produces a new image layer, and the final image will be the combined stack of all the layers.

Docker implements caching, which means that if your Dockerfile and related files haven’t changed, a rebuild can reuse some of the existing layers in your local image cache. So in our case, Docker doesn't have to download the Node base image each time we build, it will just use the cache from the last build. The layered approach means Docker can be very efficient when it builds images and runs containers.

To see a list of images you have on your local machine, run:

docker images

Running a Docker Container

We have built and tagged our Docker image. Now we can run it as a container:

docker run -p 3000:3000 docker-intro:1.0.0

run.png

The -p flag tells Docker to map port 3000 from the container to port 3000 on the host. You should be able to go to localhost:3000/video again and see the streaming video, except this time, our app is running inside a container!

Once the container has been started, you can press Ctrl-C to terminate the process and the container.

By default, Docker runs a container in the foreground. This means we can't interact with our terminal until we stop the process. To run a container in the background, we need to run it in detached mode by adding the --detached or -d option to our docker run command. This will start the container, print the container id, and then return to the terminal prompt. This way, we can continue with other tasks while the container continues to run in the background.

You can run the docker ps command to see all the containers that are running. ps is an abbreviation for "process status". Adding the -a, short form for --all, option, causes it to show all containers, both stopped and running.

To stop a running container, you can run:

docker stop <container_ID>

stop.png

To remove the container, run:

docker rm <container_ID>

Pushing an Image to Docker Hub

In our Dockerfile we used a Node image as our starting point, but where did that image come from? The answer is that it came from Docker Hub.

Screenshot (702).png

Docker Hub is a service provided by Docker for hosting, finding, and sharing Docker registries. Docker registries are used to host and distribute Docker images. You can think of it as a GitHub for container images. There are other public registries available such as Amazon Elastic Container Registry (ECR), Google Container Registry (GCR), and Azure Container Registry (ACR).


It's been announced at AWS re:Invent 2021 that AWS ECR Public is now publishing official Docker images. Having official images available on ECR Public, in addition to Docker Hub, gives developers the flexibility to download Docker Official Images from their choice of registry.

Screenshot (714).png


To be able to publish our Docker image to Docker Hub, there are some steps that we need to follow:

Step 1: Create a Docker Hub Account

Create an account by visiting https://hub.docker.com/. Any Docker user can set up a free account and store public Docker images there. Docker Personal customers have free access to Docker Desktop, Docker CLI, Docker Compose, Docker Engine, Docker Hub, and Docker official images.

Step 2: Create a Repository on Docker Hub

For uploading our image to Docker Hub, we first need to create a repository. To create a repo, first sign in to your Docker Hub account. Click on Create Repository on the Docker Hub welcome page. Fill in the repository name and click on the create button. Docker Hub's personal plan does not restrict you on the number of public repositories you can create.

Step 3: Push Image to Docker Hub

Log into the Docker public registry from your local machine terminal using the Docker CLI:

docker login

You will then be prompted to enter your Docker ID and password.

Now we have to tag our image with our docker hub username. Docker follows a naming convention to identify unofficial images, whose syntax is as follows:

docker tag <username>/<image_name>:<tag_name

In my case this would be:

docker tag deserie/docker-intro:1.0.0

Upload the tagged image to the repository using the docker push command. Once complete, you can see the image on Docker Hub. We have now successfully published our Docker image.

docker push deserie/docker-intro:1.0.0

push.png

dockerhub.png

Conclusion

We've covered a lot of ground in this article. We discussed basic Docker terminology, and looked at the key features of the underlying Linux operating system they leverage. We created a Dockerfile and used the docker build command to package our mini video streaming application in a Docker image. We then instantiated our app in a Docker container using the docker run command, and last but not least, pushed our image to Docker Hub.

In the next blog post we will explore how to push an image to AWS ECR and the different ways to run containers on AWS.

Resources

Docker Commands Cheat Sheet

Best practices for writing Dockerfiles

3 simple tricks for smaller Docker images