Containers
Before we go into the details of Docker, we first need to talk briefly about what containers are.
Containers are lightweight, portable units of software that bundle an application (often a microservice) with its dependencies, and run in isolated environments on a shared OS kernel. The key benefit the containers provide is in the fact that each container (or application) does not require a separate operating system as is the case with virtual machines. This frees up a lot of system resources such as CPU or RAM that would have been tied down to separate operating systems when running separate virtual machines.
Linux Containers
Linux containers rely on the Linux kernel. Features of the Linux kernel, such as namespaces, control groups etc, are key to how containers work. As a result, Linux containers run natively on the Linux OS by sharing its kernel and leveraging these features like namespaces for isolation and resource management
Windows Containers
Windows also supports the deployment of containers. Similar to Linux, Windows containers rely on the Windows kernel. Since the Windows kernel and Linux kernel are different, it means containers built on/for the Windows kernel cannot run or be deployed by a Linux host system and vice versa.
However, there is a caveat. There are a number of ways to deploy Linux containers on a Windows host. Two possible ways of doing this are:
- using Windows Sub-system for Linux 2 (WSL2) which provides a full Linux kernel in a lightweight VM
- using an application such as Docker Desktop that provisions a lightweight Linux VM which the containers sit on
Docker – Under the hood
We know docker is a tool for creating, running and managing containers. But what exactly is happening under the hood?
The diagram below is a good starting point
There are 3 fundamental layers to be aware of when talking about docker. They are:
- the runtime
- the daemon (or engine)
- the orchestrator

The runtime functions at the lowest level and its role is starting and stopping containers. Within the runtime, there are 2 layers:
At the low-level runtime, you have an application called ‘runc’. Runc’s job is to interface with the underlying OS and start and stop the containers.
The higher-level runtime is called ‘containerd’. containerd manages the lifecycle of the container, including pulling images, and managing the runc instances across containers. A typical docker installation has a single containerd process controlling the runc instances associated with each running container.
The daemon performs higher-level tasks like exposing the remote API, managing images, volumes, networks etc.
The orchestrator is for managing clusters of nodes running docker. Docker’s native orchestration tool is called Docker Swarm.
Your docker installation includes a ‘client’ and a ‘server’ component. You can verify this by running the command ‘docker version’
This client-server architecture within docker provides flexibility and modularity among other benefits.
The client acts as a user interface for docker. When you run commands like ‘docker run’ or ‘docker build’ from the CLI, you’re sending these commands through the client to the Docker server (daemon)
The server (also called dockerd) is responsible for managing the containers you’re interacting with through the client.
Besides flexibility and modularity, another big benefit provided by this architecture is SECURITY. The docker client doesn’t require elevated privileges to run in the way that the docker server requires because of its interaction with the underlying OS. By keeping the client and server function separate, the risk surface of the host machine is reduced.
Now that we have some understanding of the components above, here’s what’s happening when you run a docker command like docker run ubuntu echo "This is a test"
:
- Docker cli sends a request to the docker daemon
- Docker daemon tells containerd to create a container
- containerd prepares the container environment (using images, mounts etc)
- containerd calls runc to actually start the container process inside Linux namespaces
- The command (echo “This is a test”) runs and exits
- The container is stopped and cleaned up
Docker image vs container
Think of an image like a virtual machine template. An image is an object that contains an OS filesystem, an application, and the application dependencies. But it’s just packaged, and waiting to be run.
You can pull an ubuntu image using the command ‘docker image pull ubuntu:latest’
After you have pulled down the image, you can launch a container from it by running the command ‘docker container run -it ubuntu:latest /bin/bash’
From your host machine, you can see a list of all running containers by running the command ‘docker container ls’
You can attach your shell to the terminal of a running container with the command ‘docker container exec’ command. For example, if you had a container (whose container ID is 5cd20) running on your host machine, you can attach your shell to the terminal using the command ‘docker container exec -it 5cd20 bash’
Specifically, the format of the ‘docker container exec’ command is:
docker container exec <options> <container-name or container-id> <command/app>
To exit a container without terminating it, press Ctrl-PQ on your keyboard.
To stop a container and remove it, use the following commands:
docker container stop <container-name or container-id>
docker container rm <container-name or container-id>