There are some innovations in IT that everyone follows and hear about and then there are some others, quieter, humbler, those sort of innovations that hide behind a wall and when you understand their power and what you can do with them, you are truly astonished.
I guess Docker is one of those innovations in IT. When I started to read about it and started to use it during my job, one question was circulating in my mind: why I didn’t use Docker before. You know all the little incompatibilities you can have when you deliver your code? All the little things that do work during production but don’t work during deployment and you are not quite sure why? Well it’s history with Docker. It will never happen again, you are on the safe side. And all this as you create exactly the same environment during production and deployment by the use of containers. No more annoying incompatible dependencies, no more different customised settings depending on the machine. Docker is your safe heaven.
Before we go through some of Docker’s principles and how to use it, you might ask rightfully why Docker technology is preferable to virtual machines. Here I will only go through a comparison of the two, but check this article to read about virtual machines! But first:
What is Docker?
Docker actually means more than one thing. Firstly, Docker Inc is tech startup from San Francisco, initially founded as dotCloud in 2010 by Solomon Hykes but later renamed as Docker Inc. It is now the main investor of the docker technology. So secondly, docker means a technology, a tool to facilitate the use and deployment of containers. Often, when we say docker, we mean the docker engine, a software that runs and coordinate containers. Thirdly, docker also refers to an open-source project, aiming to develop a set of tools that docker uses – docker was actually part of this open source project initially, however, this was renamed in 2017 to the moby project.
The name Docker comes from the concatenation of two words: dock and worker. (You will shortly see why.)
Altogether, docker is a software that creates, manages and coordinate containers. It has Windows and Linux versions. It is mainly managed by Docker Inc. and was developed as part of an open-source project (initially called docker but now renamed to moby). It has a community and an enterprise edition. To dive into the docker technology, let’s then talk about virtual machines, containers and why we need them.
A proper understanding of containers is essential in the Docker World. To understand why we needed them in the first place, let’s go back in time. Businesses are based on applications but in the old days, one application ran on one server. This led to a waste of server space. Suppose for instance that the company needed a new server. How big server did they actually need? Difficult to estimate, so they invested in a large server (for lots of cash) and finally they could only use say 20% of the sever space for a given application. Could we use the remaining resources? Would it be possible to share a sever across many applications? The answer soon became yes, when virtual machines appeared. Virtual machines (VMware) divide the server to several slices, so called virtual machines, and each of these virtual machines can host an application. They have their own operating systems, disk space, and everything else the original server could host. They share the resources of the original server, so now we can host more than one application on one server and we can optimise the server. Right? Well, not quite so. VMware was a great forward step in IT, however, there was still a problem. The structure of the VMware require one operating system per virtual machine. This is a waste of disk space. Add this to the cost of wasting all these resources to the additional licence costs, admin costs and updates and you can see that VMwares are not optimised. They waste some of the resources we had on the server. Of course this was still better than having one application on one server and by no mean I underestimate the importance of VMware. But.. Along came containers.
Let’s compare the way virtual machines organise resources to how containers do. Containers allow the use of several “operating system slices” for different applications just as virtual machines do, but instead of installing several OS for each virtual machines, containers install only one, then slice it. So we can run our applications inside the containers, but now as the OS is not installed in each container, it is smaller and more optimised compared to the virtual machine. Why do we care that a container is smaller than a virtual machine? Well smaller means that it is much faster to build and/ or run. The following schema shows how the the virtual machines structure vs the container infrastructure (in the example we build 3 containers and virtual machines).
Getting started with docker
There are lots of different dockers you can download: docker for Linux, docker for Windows, for Mac, for cloud, for premises, for desktop.. So I don’t write here about how to download docker, juste have a look here instead!
When you install docker, you actually install two components: the docker client and the docker engine (this latter can be called docker daemon or server). You can check that your docker is good to go by the terminal command:
$ docker version Client: Docker Engine - Community Version: 19.03.5 API version: 1.40 Go version: go1.12.12 Git commit: 633a0ea Built: Wed Nov 13 07:22:34 2019 OS/Arch: darwin/amd64 Experimental: false Server: Docker Engine - Community Engine: Version: 19.03.5 API version: 1.40 (minimum version 1.12) Go version: go1.12.12 Git commit: 633a0ea Built: Wed Nov 13 07:29:19 2019 OS/Arch: linux/amd64 Experimental: false containerd: Version: v1.2.10 GitCommit: b34a5c8af56e510852c35414db4c1f4fa6172339 runc: Version: 1.0.0-rc8+dev GitCommit: 3e425f80a8c931f88e6d94a8c831b9d5aa481657 docker-init: Version: 0.18.0 GitCommit: fec3683
This shows that the docker daemon and the client is running, and that they are communicating. The docker engine is the core software that manages containers, it has a modular design and it follows (mostly) the Open Container Initiative (OCI) standards. (The OCI provides guidelines for standardising the container and image formats.) The docker engine (often referred to simply as docker) contains the following components: daemon, containerd, runc. The following schema shows how the docker engine is structured:
The docker daemon implements the docker API, containerd manages container execution logic, such as start | stop | rm | pause | etc … commands. It can be thought of as a container supervisor that handles container lifecycle operations. Runc a container runtime tool, it creates containers. It communicates to the containerd, and this latter ensures that docker images are passed to runc.
We can think of images as stopped containers or containers as running images. They are small and lightweight, made up of multiple read-only layers, stacked on the top of each other and they represent a single object. The following schema shows the structure of an image (in the example with 5 layers):
We can pull images from an image registry (the most commonly used is the Docker Hub) The command
docker image pull ubuntu:latest gets the ubuntu image. (If you have not yet registered and the pull command doesn’t work, try
sudo docker image pull ubuntu:latest) Now you can see all local images with the
docker image ls command. When we run the command
docker image pull, we add the repo and the version of the image we want to download. So for instance
docker image pull ubuntu:latest will get the latest stable ubuntu image (so repo: ubuntu, tag: latest), whereas the command
docker image pull mongo:3.3.11will pull the 3.3.11 version from the mongo repo. The Docker Hub is not the only public directory, but there are lots of images validated by Docker (so stable, bug-free ones) and there are other, private images. Private images can also be good and stable, we just need to make sure that we know what we use. Finally, if we don’t specify the tag name, docker will get the latest image.
Let’s run then the command:
$ docker image pull mongo:3.3.11 3.3.11: Pulling from library/mongo 357ea8c3d80b: Pull complete 1a0ccbb2e7a6: Pull complete 52e1236315cf: Pull complete 42a65768c956: Pull complete 5c7b6aa39259: Pull complete 0b7b0bedb2b2: Pull complete 9d7bf9ec8540: Pull complete 6309ee1b659d: Pull complete ce80fdeca717: Pull complete Digest: sha256:95d63cfa52f476b196e004ea54108bb876e76825426c2d489610796eb315d68b Status: Downloaded newer image for mongo:3.3.11 docker.io/library/mongo:3.3.11
We can see that all nine layers of the mongo:3.3.11 image was pulled, and together they creates the image.
We can also list all images by the
docker images ls command.
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE alpine latest a187dde48cd2 43 hours ago 5.6MB ubuntu latest 4e5021d210f6 4 days ago 64.2MB mongo 3.3.11 97d969a5eca5 3 years ago 425MB
docker images rmcommand, indicating which image to remove (here by the hash id):
$ docker image rm 97d969a5eca5 Untagged: mongo:3.3.11 Untagged: mongo@sha256:95d63cfa52f476b196e004ea54108bb876e76825426c2d489610796eb315d68b Deleted: sha256:97d969a5eca592559b9ab5f35a4b8fc2984fabe99fae6e34e5fa37809705bffa Deleted: sha256:6ac4d19b57a804dd21d74ebe6ad4d9740bd153e4aeb47e140f1b433ad948a159 Deleted: sha256:b4fb48366de4847e385d0fe23d5d558dfb57261afae6afb4f5e1a72feab1357d Deleted: sha256:fee5e81fe81d5c228f84c2c2ef739611209783bbab046cc13efd1ac632c712cb Deleted: sha256:64a35dac6e2cead2dd61d5b37ed322f6de21dd9996ea62e2ee1bc0d82afb7027 Deleted: sha256:355cc478c9ecbc0dbb7f8170b9038985eb022ffe1974ad6208cefcbdb7ea9f0d Deleted: sha256:a6585f965159b0c49cd9e637a202906d09a85487073b74dec911c4b1effcec2c Deleted: sha256:6858d196766546d8573e5ad523f30e2941888ab98c4ba03c04004ddf1724f3b2 Deleted: sha256:a9ed4ab99f1da7b792d706736e537fff6d3df929184c5719594b034653c6d2c1 Deleted: sha256:2f71b45e4e254ddceb187b1467f5471f0e14d7124ac2dd7fdd7ddbc76e13f0e5
And not surprisingly, all 9 layers of the image are deleted.
Until now we pulled images, an another way to have an image is to create one. This is what I will talk about just after a quick introduction to the cousins of the images, the containers.
The Docker container is the runtime interface of an image. That is, we can think of containers as running images, or of images as stopped containers. A single image can be used to start one or more containers. We can start a container by the
docker container run command. We need to specify which image to use to create the container and we can specify the application we wish to run in the container. The “-it” precision lances the container with an interactive shell (so in our terminal):
$ docker container run -it ubuntu
Then we can list the containers:
$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c178cbae1014 ubuntu "/bin/bash" 3 minutes ago Up 3 minutes strange_hertz
docker execcommand. For instance the following command access the container and runs terminal.
$ docker container exec -it c178cbae1014 bash
We can exit the container’s bash shell by pressing
Now just because we exited this container, it is still up and running. To stop it, we can use the following command (change the <
container_id or container name > for the name or container id):
$ docker container stop < container_id or container name >
$ docker container rm < container_id or container name >
Containerising an application – example
Do you remember why we wanted to use Docker on the first place? Historically, people wanted to containerise apps, so that more than one app is on one server. We, however, also wanted to get rid of any incompatibility issues between development and deployment, any annoying little bugs that can be caused by the different interface. So let’s use Docker for this then, let’s containerise an app, a very simple app that creates a web interface and displays a short message (let’s keep it secret for the moment just to motivate us all)! So what does it mean, containerise an app? It means that we create a Dockerfile, that contain instructions about the app, contains its dependencies and information on how to run it. Then to lance the app, we feed this Dockerfile to Docker in order to create an image from it. Once it’s done, we have a Docker image, we only need to create a container from it and run the app in the container! The following schema shows John, our friend passionate for the programming and Docker. He containerises an app by creating a Dockerfile, then an image, from the image a container and run his app in the container.
Let’s look at my very simple web app example then, and let’s containerise it! You can pull the folder from my github:
$ git clone https://github.com/fannihalmai/SimpleContainerisedApp.git Cloning into 'SimpleContainerisedApp'... remote: Enumerating objects: 17, done. remote: Counting objects: 100% (17/17), done. remote: Compressing objects: 100% (14/14), done. remote: Total 17 (delta 2), reused 11 (delta 0), pack-reused 0 Unpacking objects: 100% (17/17), done.
$ zip SimpleContainerisedApp ./SimpleContainerisedApp $ cd SimpleContainerisedApp $ ls
Dockerfile README.md circle.yml my_app.js package.json test views
The folder contains the app, and a Dockerfile. The Dockerfile aims to tell Docker how to containerise our apps and to describe the application. Let’s look at the Dockerfile and see what it does:
# Linux x64 FROM alpine # Install npm and node RUN apk add --update nodejs nodejs-npm # Copy the app to the /src folder COPY . /src # Make the /src folder as working directory WORKDIR /src # Install dependencies RUN npm install EXPOSE 8989 ENTRYPOINT ["node", “./my_app.js"]
The Dockerfile says to Docker: get the alpine image and start with it. Everything else that comes after the first command line will be in the next layers of the image. Then, Dockerfile tells Docker to install node.js and npm, copy everything from the present directory to the src folder of the container, define it as a working directory, install some dependencies (npm), add a network port and run my_app.js.
We can create an image from this Dockerfile with the following command:
$ docker image build -t simple_web_app:latest .
Sending build context to Docker daemon 83.46kB Step 1/7 : FROM alpine ---> a187dde48cd2 Step 2/7 : RUN apk add --update nodejs nodejs-npm ---> Running in 7fa07370e18b fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz (1/8) Installing ca-certificates (20191127-r1) (2/8) Installing c-ares (1.15.0-r0) (3/8) Installing libgcc (9.2.0-r4) (4/8) Installing nghttp2-libs (1.40.0-r0) (5/8) Installing libstdc++ (9.2.0-r4) (6/8) Installing libuv (1.34.0-r0) (7/8) Installing nodejs (12.15.0-r1) (8/8) Installing npm (12.15.0-r1) Executing busybox-1.31.1-r9.trigger Executing ca-certificates-20191127-r1.trigger OK: 64 MiB in 22 packages Removing intermediate container 7fa07370e18b ---> c7bfce1cd855 Step 3/7 : COPY . /src ---> 09f2dd197ae8 Step 4/7 : WORKDIR /src ---> Running in e7c15cd36658 Removing intermediate container e7c15cd36658 ---> 0124cea206cc Step 5/7 : RUN npm install ---> Running in 6bb9a17717d2 Step 6/7 : EXPOSE 8989 ---> Running in 60bfb778e587 Removing intermediate container 60bfb778e587 ---> 164f6cbfb92b Step 7/7 : ENTRYPOINT ["node", "./my_app.js"] ---> Running in 09fed1943adc Removing intermediate container 09fed1943adc ---> 6a0b5c08b043 Successfully built 6a0b5c08b043 Successfully tagged simple_web_app:latest
This command tells docker to build an image from the Dockerfile (.) that will be called simple_web_app: latest (repo:tag). Next we run a container from the image (if you have an error message
permission denied use the
sudo before the command):
$ docker container run -d --name simple_app -p 8989:8989 simple_web_app:latest dc5a619a4e72895f41fdc5cb8d451c00737c9fa7ab34c2ee0f902c1e6ed266c4
And now we are running our web app in a container! To see it, go to
http://localhost:8989/! And that’s it, we successfully containerised our app. I hope you like the simple web app and thanks for reading!
I really appreciate this book, I read it and if you want to understand Docker more, I strongly recommend you to read it: Nigel Poulton: Docker Deep Dive. Thanks Nigel!