In our last post, we explained how docker works on a client-server architecture and also talked about the different docker components. In this article, we will discuss the docker engine itself in greater detail. The docker engine is the heart of what runs and manages containers. It has been described as batteries included but replaceable to reflect it’s a modular design. Most of the engine is based on open standards outlined by the Open Container Initiative.
The major components of the docker engine are as follows:
- Docker client
- Docker daemon
These components work together to create and run containers. Before we talk about the modular docker container engine, we’ll briefly go through how the docker engine looked earlier in its monolithic incarnation.
The first release of docker consisted of two components: the docker daemon and LXC. Initially, the docker daemon consisted of a plethora of components bundled into it. Some of those components are listed below:
- Docker client
- Docker daemon
- Monolithic binary
- Docker API
- Container runtime
- Image builds
LXC provided docker with the fundamentals building blocks of creating containers in Linux i.e. namespaces and control groups. LXC is exclusively meant for Linux and therefore the then docker-engine built using LXC could only run Linux containers on the Linux platform.
In order to provide the required flexibility to the docker engine to run containers for other platforms besides linux, Docker Inc. refactored the docker engine replacing LXC with libcontainer.
libcontainer was designed to be platform-agnostic and provided docker with the fundamental building blocks that existed in the Linux kernel allowing Docker to be run on Mac OS and Windows platforms. In Docker 0.9, libcontainer officially replaced LXC.
Modularising the Docker daemon
The existing docker daemon had become slow and harder to innovate because of it’s monolithic nature. In order to build a more modular ecosystem, the Docker daemon was broken up into smaller and more specialized tools which we will talk about now.
In 2015, the Open Container Initiative was established by Docker and other leaders in the industry. Its purpose is to create open standards around image formats and container runtime. At present, there are two standards called the image spec and container runtime spec. As of Docker 1.11 (2016), the docker daemon did not contain any of the runtime code and all of it was now implemented in an OCI compliant layer called runc. runc is the implementation of the OCI container runtime spec and is a lightweight wrapper around libcontainer. The sole purpose of runc is to create containers.
As part of the refactoring of the docker daemon, all the code logic responsible for managing the container life cycle was moved to containerd. This includes starting, stopping, pausing and deletion of containers. In addition to this containerd is also responsible for image management i.e. the push and pull of docker images. It officially became a part of Docker in the 1.11 release.
The shim layer is responsible for decoupling of running containers from the docker daemon. When a new container is created containerd forks an instance of runc. The runc process exits after the container has been created and a new shim process becomes the parent process of the container. This allows us to run hundreds of containers without running hundreds of runc instances. The shim process is responsible for keeping STDIN & STDOUT open so that the container does not terminate in case the docker daemon is restarted. It’s also responsible for reporting the container exit process to the docker daemon.
Given below is the step by step process on what happens when we execute the docker run command:
1 Use the CLI to execute a command.
2 Docker client uses the appropriate payload and POSTs to the correct API endpoint.
3 The docker daemon receives the instructions.
4 It calls containerd to start a new container using gRPC(a CRUD style API).
5 containerd creates an OCIbundle from the docker image and tells runc to create a container using the OCI bundle.
6 runc interfaces with the kernel to get the constructs needed to create a container (namespaces, cgroups, etc)
7 The container process starts as a child process.
8 Once the container starts the runc process exits and a shim process becomes the parent process of the container.
This completes the start-up process and the container is now in running state.
This concludes our explanation of the various components that make up the docker engine. We hope that you found this post to be useful and look forward to your suggestions and feedback.
Latest posts by Sahil Suri (see all)
- Ansible install on RHEL 8 explained - August 22, 2019
- RHEL 8 server registration with Red Hat subscription Management - August 20, 2019
- RHEL 8 installation step by step with screenshots - August 19, 2019
- 3 ways to obtain per process swap utilization in Linux - August 16, 2019
- Docker engine explained - August 15, 2019