Docker is an open-source virtualization platform. It went open source in 2013 and has seen rapid adoption in companies since then. Docker offers OS-level virtualization to package software such as web servers, databases into containers. This essentially removes the virtual machine overhead like VirtualBox, VMware and other virtualization technology depending on virtual machines. Containers allow to maintain software in complete isolation without impacting the whole host system with dependencies and modifying global files.

This tutorial will help you get familiar with Docker and kick off your journey with containers.

Docker Architecture

Below is a diagram from Docker’s official website showing Docker architecture:

Here is a brief explanation of the different sections:

SectionsDescription
ClientThe client is the command line interface or CLI that allows you to run docker commands
HostThe host contains all the essential files and software for docker to run. It will contain images you downloaded or built and also the running containers.
RepositoryThis makes the life of developers and administrators much simpler and it also allows for reproducible environments across different systems. Software publishers often release their software as containers and make them available in the docker hub or repository.

Hands-on

Installation

Docker can be installed on Linux, Mac and even Windows. If you want to try from Windows and WSL, be sure to have WLS2 installed. Docker already has a complete documentation for its installation. Head to Get Docker page to properly install Docker for your environment.

Pulling Images

Once Docker is installed, you can pull any image you want from the Docker Hub repository. For this tutorial, let’s start by pulling Nginx, which is one of the well known web servers:

$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
45b42c59be33: Pull complete
d0d9e9ea897e: Pull complete
66e650438339: Pull complete
76a3dfe4406b: Pull complete
410ff9d97480: Pull complete
Digest: sha256:8e10956422503824ebb599f37c26a90fe70541942687f70bbdb744530fc9eba4
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest

Once the download is completed, you can list your available images:

$ docker images
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
nginx                    latest              298ec0e28760        7 days ago          133MB

The image is only the files needed to run the container. The container (running software) will be created from the image. You can run many containers out of a single image!

Create and run a container

A container can be run either in foreground (interactive) or detached mode. If you want to interact with the container in a similar as when you login a system, then use the foreground mode.

Launching a container with the bash shell:

$ docker run -it nginx /bin/bash
root@eed549b099b1:/#

The switch -i launches the container in interactive mode (it will keep STDIN open) and -t will allocate a pseudo-TTY to the container.

The command run actually creates the container from the image nginx. The container will be destroyed once you exit the container.

Setting a hostname when creating a container:

$ docker run -it --hostname="webserver" nginx /bin/bash
root@webserver:/#

You will also notice that a bunch of common commands will often not be installed on the images:

root@webserver:/# ip a s
bash: ip: command not found
root@webserver:/# ping localhost
bash: ping: command not found

This is because the containers are designed to be as lightweight as possible, while providing the respecting services, in this case Nginx.

Let’s now use our Nginx image to run a container listening on port 80:

$ docker run --hostname="webserver" -p 80:80 -d nginx
601334ad39429691636e1d6e151492a38263ec8a23df9637621a8a5993db0834

We used -d to run the container in the background, and we publish a container’s port to the host using the -p parameter.

-p 80:80 means it maps port 80 on host machine to port 80 on the container. For example, if you want to run Nginx on port 8080 on your host machine, then you set it to 8080:80.

To check if the container is actually working, the following docker command does that:

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
601334ad3942        nginx               "/docker-entrypoint.…"   2 minutes ago       Up 2 minutes        0.0.0.0:80->80/tcp   vibrant_bell

Notice the port mapping on the port 80, this is what allows you to connect to your Nginx container. The mapping was done with -p 80:80. If you check on your host, you’ll notice that the port 80 is open:

$ ss -ntlp
State                      Recv-Q                      Send-Q                                              Local Address:Port                                             Peer Address:Port
LISTEN                     0                           4096                                                            *:80                                                          *:*

We can now access our webserver :

$ links -dump http://localhost
                               Welcome to nginx!
​
   If you see this page, the nginx web server is successfully installed and
   working. Further configuration is required.
​
   For online documentation and support please refer to nginx.org.
   Commercial support is available at nginx.com.
​
   Thank you for using nginx.

The above command uses links cli web browser command to see whether Nginx server is working.

If the above command raises the command indicating the command not found, you can easily install links with apt:

$ sudo apt update
$ sudo apt install links

Execute command on running container

To run a command on a running container, we need to first have the container’s ID:

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
601334ad3942        nginx               "/docker-entrypoint.…"   18 minutes ago      Up 18 minutes       0.0.0.0:80->80/tcp   vibrant_bell

Now that we have the ID, we can run the command with exec instead of run. Here we are running the command ls on the container:

$ docker exec 601334ad3942 ls
bin
boot
dev
docker-entrypoint.d
docker-entrypoint.sh
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
www

Persistent data via volume

Data in a container is not persistent. Once the container is stopped, you will lose all written data. Let’s try it out!

We will create a file on the running container with touch:

$ docker exec 601334ad3942 touch /home/mydata
$ docker exec 601334ad3942 ls /home/mydata
/home/mydata

We can see that the file is present. Now let’s stop the container and launch another one from the same image:

$ docker stop 601334ad3942
$ docker run -it --hostname="webserver" nginx /bin/bash
root@webserver:/# ls /home/
root@webserver:/#

The previous file mydata is no longer available as we created a new container. To make changes persistent, you need to create a volume. Let’s delete the previous container, by using exit command as we’re not running in detached mode:

root@webserver:/# exit
exit

Now let’s create a volume that will be used by the new containers:

$ docker volume create mydata
mydata
$ docker volume ls
DRIVER              VOLUME NAME
local               1e95a7a656a43a1520360a02377fe412a9faf143b4e332530ae654f9c08fcd5f
local               2f068575e3d908632eb544a47ec6e5f38fc71af664949c627f12b38d8d1aa9d9
local               5ef03800185c1c97c2b1d7a5eac22da0b386ac06364d2c140d24ad6625f05d5b
local               6f03a2fa00fabbe9197d341b2f77d1f861e47c2147025e0532834d59a925123b
local               22a4a43e62fb4f431cbd792e08ff9b3315c43d2a4004aec141b09af71b354b09
local               74df639086ba283ab5cce14f65c89fb37a3567778c0c1634d2ed0e339bf8f226
local               77e9359d8b2e88c79a7afcb6b2e2e841e72f80dffc8b2e6982abb7fc33c2000b
local               81ccb23561b762ebcda18d9721ab9121a176d63494faf1ef004806d3651be370
local               92ed8a1996b5cdb4b7de4d4925ac8864c9a6223849d201404ba635083f9dc0df
local               96905d53451acd5a7b83bab34fec58799a1a8db341c6bdf1d5dd373bf5a1fa3b
local               3935083fc00b5f71debab1ba65698355834451b85daf050906c8f2d4598703a0
local               abbd864f6a8cb8c811f16fdf768ff87b3d2de0bb35d893a7f771f325e2765dce
local               b74916985a90518afae85a85a9d2a2ed07be1a7f07489596d61c3e0c829a5c20
local               ce672e5df74afe95480fa5f32726355a6cea7558e8fdfcf01380745159296439
local               daa7e48e91f2c8175f56ad5e8ec5901cb82701970b8da779eec8ec087ce09b7b
local               dc0048dd8d6b4597c275b30d877bb8fe0957baccf94c91474bee8906e6db9704
local               f7aa4b5534a0e27897f1df2e5aacfec0a37c3a94b3571399a5fa9c55738ff200
local               fefff75043229c4febe3df4cae265f894fee436baaf50ca999d6a0f3f54a4f64
local               fff7933bf781a1f077077dd485d8cd3bf59f2864ebf574f254723c6ac195ecc5
local               mydata

We’ll then create a container webserver1 and make it use the volume mydata, mounting it on /mydata in the container:

$ docker run -it --hostname="webserver1"  --mount source=mydata,target=/mydata nginx /bin/bash

Let’s create a file in the mounted volume within the container:

root@webserver1:/# echo "shared data" > /mydata/info.dat

We can now create a second container which we will name werbserver2, connecting it to the same volume and check if we can see the previously created file:

$ docker run -it --hostname="webserver2" --mount source=mydata,target=/mydata nginx /bin/bash
root@webserver2:/# ls /mydata/
info.dat

It worked!

The volume mydata will still exist even after both containers are stopped.

Docker Compose

To create complex platforms with multiple inter-connected containers, we can use Docker compose files:

We will build a WordPress with a web server (Nginx) and a database (MySQL) using a compose file:

$ mkdir wordpress
$ cd wordpress/
$ vim docker-compose.yml

Fill the docker-compose.yml file with the following configuration:

services:
   db:
     image: mysql:5.7
     volumes:
       - db_data:/var/lib/mysql
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: somewordpress
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: wordpress
​
   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     ports:
       - "8000:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD: wordpress
       WORDPRESS_DB_NAME: wordpress
volumes:
    db_data: {}

Now, we’ll build the project with docker-compose up -d:

$ docker-compose up -d
Creating network "wordpress_default" with the default driver
Creating volume "wordpress_db_data" with default driver
Pulling db (mysql:5.7)...
5.7: Pulling from library/mysql
45b42c59be33: Already exists
b4f790bd91da: Pull complete
325ae51788e9: Pull complete
adcb9439d751: Pull complete
174c7fe16c78: Pull complete
698058ef136c: Pull complete
4690143a669e: Pull complete
66676c1ab9b3: Pull complete
25ebf78a38b6: Pull complete
349a839d5e27: Pull complete
40b03e3e5980: Pull complete
Digest: sha256:853105ad984a9fe87dd109be6756e1fbdba8b003b303d88ac0dda6b455f36556
Status: Downloaded newer image for mysql:5.7
Pulling wordpress (wordpress:latest)...
latest: Pulling from library/wordpress
45b42c59be33: Already exists
a48991d6909c: Pull complete
935e2abd2c2c: Pull complete
61ccf45ccdb9: Pull complete
27b5ac70765b: Pull complete
5638b69045ba: Pull complete
0fdaed064166: Pull complete
e932cec09ced: Pull complete
fbe190145b1c: Pull complete
f747612094ef: Pull complete
300f68c220b1: Pull complete
efd583fc4f80: Pull complete
011e53c9540e: Pull complete
90d05db0a960: Pull complete
5faae26e6219: Pull complete
7bf1209c35d8: Pull complete
527f0104274c: Pull complete
435b4e30e1cf: Pull complete
2c8618e23e3e: Pull complete
38bf6a404b0c: Pull complete
Digest: sha256:a0a54e8405881281cbaea36553d7700a07cfd60c062fd2e2d021452d600c1fcc
Status: Downloaded newer image for wordpress:latest
Creating wordpress_db_1 ... done
Creating wordpress_wordpress_1 ... done

Once everything is complete, head to your browser on port 8000 and you should be able to access WordPress’ installation page as shown in the image below:

Cloud deployment

Docker can also be deployed on cloud platforms such as AWS, Azure and GCP. AWS containers can be launched with either lightsail or fargate for instance. Platforms such as Heroku, which also support docker deployments might be preferred by developers.

Conclusion

There is so much more to explore with Docker and we hope that this tutorial has provided you enough knowledge to start your journey with containers. Container knowledge is now pre-requisite for most IT and systems jobs. Try out other images from the Docker hub, there are images of every development software you can imagine.

For more information, I suggest you read the Docker’s official documentation.

Have fun exploring the world of OS-level virtualized container!