Why no new project should see the light of day without docker

Oct 16, 2023 min read

Docker, what is it, exactly?

Let’s place ourselves at the level of an application.

Its lifecycle is composed of 3 major steps:

  • Development
  • Tests
  • Production

In these different environments, and in an ideal world, the application should behave in exactly the same way. Why would there be differences where there shouldn’t be any?

Why when I’m going to install an application on certain servers, does it work wonderfully, and on others not, even though I don’t change anything to my installation?

The answer is simple: the system environment influences the application. and unless we test all possible environments on earth, you’ll never be sure that your application will work everywhere as it does on the developer’s workstation.

Is it inevitable? No, of course. But there’s a possibility other than a battery of tests as costly in time, as in money:

And if we created a box, which, itself, would be compatible with all environments and which would isolate our application from the rest of the system?

The idea may seem illusory, but it’s on this concept that Docker is built. Docker is “a chroot plus cgroups”. In summary, we create a file system independent of that of the system, we give it isolated and privileged accesses to the kernel (core of the operating system) and we add to it network virtualization for communication of docker applications with the host system and the rest of the world.

This looks enormously like a virtual machine. We’ve been doing this for years with vmware, proxmox, xen, etc… Why use a new tool when we already have everything at hand?

Well, if there are many resemblances, there are also enormous differences. In terms of resources already, because there’s no need to emulate a complete operating system. Docker is a light virtualization solution that relies on the host’s system to function. But what makes its main strength also makes its weakness. We can’t run certain applications if they’re not compatible with the host’s kernel. We think directly of windows applications that won’t run on linux systems via docker. The reverse not being true either by the way. However, on windows servers now, a linux OS virtualization layer allows running Linux applications. But they don’t run on the windows kernel, but well on a linux kernel.

Another notable difference is that a docker container must not be considered as a complete virtual machine. Too many rights given to your applications in the container can create security flaws on the host system. Docker volumes, which allow sharing the host’s file system in a container and root rights inside this container can have unexpected and unfortunate consequences.

Another difference, a docker container’s memory footprint is reputed quasi-null (I’ve never benchmarked an application to the point of being able to affirm it, but experience goes in this direction). Which means your application will run with the same performances as if it had been installed natively directly on the operating system.

But let’s get back to our sheep: why do I say that no application should see the light of day without Docker today?

Let’s take the case of a python developer. we know that python is a language that poses many compatibility problems between its versions. The developer will therefore juggle between these versions for their developments (It’s rare to have only one app to develop, and not have “legacy” to maintain). They will therefore spend costly time switching between environments. Applications they’re going to produce will also have to be tested and exploited on different environments, because incompatible with each other.

Complexity will follow applications throughout their lifecycle, which will be costly at all levels for dev, test and operations teams. Let’s not talk about errors, which are human, and unfathomable monolithic apps that only work in an environment with care, with obsolete and unfindable libraries and that no one really masters.

Docker brings answers to these cases.

How does Docker work?

Docker relies on a daemon (known as a service, in the windows world), which will be installed on a machine, and which will make the link between containers we’ll ask it to run and the host system.

From a Dockerfile, we produce an image. Thanks to these images, we’re going to start containers. A container is an instantiation of an image.

Basically, it’s as if you had a stamp made with your name. The stamp is the image, and the container is your name stamped on a sheet. The stamp has no other purpose than to repeat infinitely and with exactitude and always in the same way your name on all sheets presented to it.

Building a docker image is done by “layers”. We take an existing image, we add/modify files, applications and we produce a new image.

An example of a fairly basic Dockerfile (file where we describe image construction)

FROM alpine
COPY helloworld /run/
WORKDIR /run/
CMD [ "/run/helloworld", "" ] 

The “FROM” indicates the image from which I create my own image. Here it’s a minimal image available on docker hub, at address https://hub.docker.com/_/alpine

COPY allows copying files from the file system of the machine on which you’re building the image (developer workstation, jenkins, gitlab runner, etc…) to the image’s file system. Here, I copy a self-contained Go binary “helloworld”, present in the folder in which I’m building my image, to the /run/ of the image.

WORKDIR sets the folder from which the container will launch the command

Finally, CMD specifies the command the container will launch when executing. It won’t do anything else.

If we summarize, the file downloads an existing image, adds a binary to it, defines the folder in which it must execute and the command to launch it.

The following command creates the image from the Dockerfile and files present in the folder from which we execute it, the “.” defining the current directory under linux.

docker build .

For practical questions, we’ll add the -t nomdelimage option which will allow calling it afterwards

docker build -t helloworld .

The console output is the following

$ docker build -t helloworld .
Sending build context to Docker daemon  3.296MB
Step 1/4 : FROM alpine
 ---> 961769676411
Step 2/4 : COPY helloworld /run/
 ---> 1692ce89a4f5
Step 3/4 : WORKDIR /run/
 ---> Running in 5d81b369798f
Removing intermediate container 5d81b369798f
 ---> 5d26ad1e983d
Step 4/4 : CMD [ "/run/helloworld", "" ]
 ---> Running in 54115b068924
Removing intermediate container 54115b068924
 ---> 494f734a76da
Successfully built 494f734a76da
Successfully tagged helloworld:latest

Then we can execute the container directly with the command

docker run helloworld

Where “helloworld” is the name we defined with the “-t” option during build. The console output therefore gives:

$ docker run helloworld
hello world ! 

It’s as simple as that.

Now, let’s say I want to change the base image for my application. I now want it to run in an ubuntu system, but version 18.04. I therefore modify my Dockerfile accordingly, renaming it Dockerfile.1

FROM ubuntu:18.04
COPY helloworld /run/
WORKDIR /run/
CMD [ "/run/helloworld", "" ] 

I rebuild my image, but this time specifying that the Dockerfile has changed, with the “-f” option

docker build -f Dockerfile.1 -t helloworld .

The output:

$ docker build -f Dockerfile.1 -t helloworld .
Sending build context to Docker daemon  3.297MB
Step 1/4 : FROM ubuntu:18.04
18.04: Pulling from library/ubuntu
23884877105a: Pull complete 
bc38caa0f5b9: Pull complete 
2910811b6c42: Pull complete 
36505266dcc6: Pull complete 
Digest: sha256:3235326357dfb65f1781dbc4df3b834546d8bf914e82cce58e6e6b676e23ce8f
Status: Downloaded newer image for ubuntu:18.04
 ---> c3c304cb4f22
Step 2/4 : COPY helloworld /run/
 ---> 6d55356eab72
Step 3/4 : WORKDIR /run/
 ---> Running in 84bf593deac4
Removing intermediate container 84bf593deac4
 ---> f35025038e86
Step 4/4 : CMD [ "/run/helloworld", "" ]
 ---> Running in db718029f8db
Removing intermediate container db718029f8db
 ---> bb3d5526bfad
Successfully built bb3d5526bfad
Successfully tagged helloworld:latest 

And there, failure. My application is too old and doesn’t support ubuntu 18.04. It’s not serious, we’re going to test with ubuntu 16.04. I modify my Dockerfile:

FROM ubuntu:16.04
COPY helloworld /run/
WORKDIR /run/
CMD [ "/run/helloworld", "" ] 

The output:

$ docker build -f Dockerfile.1 -t helloworld .
Sending build context to Docker daemon  3.297MB
Step 1/4 : FROM ubuntu:16.04
16.04: Pulling from library/ubuntu
e92ed755c008: Pull complete 
b9fd7cb1ff8f: Pull complete 
ee690f2d57a1: Pull complete 
53e3366ec435: Pull complete 
Digest: sha256:db6697a61d5679b7ca69dbde3dad6be0d17064d5b6b0e9f7be8d456ebb337209
Status: Downloaded newer image for ubuntu:16.04
 ---> 005d2078bdfa
Step 2/4 : COPY helloworld /run/
 ---> 6878dcea9a4d
Step 3/4 : WORKDIR /run/
 ---> Running in 79313eb14c81
Removing intermediate container 79313eb14c81
 ---> acdb5b4a6598
Step 4/4 : CMD [ "/run/helloworld", "" ]
 ---> Running in 92ad4570e082
Removing intermediate container 92ad4570e082
 ---> ca8486b110d1
Successfully built ca8486b110d1
Successfully tagged helloworld:latest 

And there magic, it works! By changing a simple line, I’ve corrected an environment problem.

$ docker run helloworld
hello world ! 

No need to change OS, install new libraries, lose precious time debugging the system as much as the application.

Now, I’m going to be able to distribute my application while knowing that it will remain in the environment I’ve decided to provide it, and this on all linux machines where it will execute.

It seems simple, too simple. But it’s the case. The ecosystem is much richer than what I can show you here and covers the great majority of cases.

Well, and my python dev in this affair?

They too will be able to benefit from environment portability around the application.

Let’s say, they develop a bonjourlemonde.py application, compatible python 3.8.3 whose code is the following:

print("Bonjour le monde !")

And the Dockerfile (Dockerfile.python3):

FROM python:3.8.3-buster
COPY bonjourlemonde.py /run/
WORKDIR /run/
CMD ["python3","bonjourlemonde.py"]

The build will not only construct a python environment in the precise version the developer expects, but will also assure them that tests and production will be done in the same conditions. It’s the dev who chooses run conditions and provides their application’s image.

This image can have as source an image created by system teams, managing application security and scalability issues.

Moreover, we’ll be able to run applications of different python versions on the same machines, without there being collisions, since applications are locked in their container, containing their execution environment and their own file system.

We can retrieve environments directly created by publishers

You need to install an application necessary for a development environment? Docker is there.

Example: for needs of caching precalculated data, your application uses redis. On the developer’s workstation, redis is also needed to test caching. The developer has no system admin knowledge, nor has ever installed redis.

They’ll only have one command to launch

Docker run -p 6379:6379 redis

Docker will take care of downloading the official redis image, decompressing it and launching the application, which moreover will be isolated from the rest of the system and will have no impact on the development environment. The application will listen on port 6379 of localhost as if we had just installed it.

What’s true for redis will be true for thousands of applications. I’ve personally always found the applications I needed. You can even add your own.

In conclusion:

Docker allows delivering applications in an “already installed” container, containing the application, as well as all its dependencies, all already installed and ready to run.

You can also quickly deploy applications, internal or external to your company/project in a few seconds to create development or production environments for yourself.

Using docker-compose can extend this case ( See my post on terraform to have a glimpse )

We could extend this post on all application cases we can encounter, but I’ll let you do your research. It’s by digging yourself that you learn. The goal here was only to pique your curiosity by presenting the concept and allow you to do your research behind.

Personally, I can no longer do otherwise. I hope it will be the same for you.

-|