§15.4.

Containerization

Containerization in modern operating systems is a technology for efficiently running processes in isolated environments.

Containerization is similar to but different from virtual machines. A virtual machine is an entirely isolated virtual computer that can run an operating system. With containerization, there is just one shared operating system. The operating system provides isolated environments so that programs appear to be running on a separate computer.

How containerization works

Your operating system consists of two parts:

The kernel

The kernel is the core of your operating system. It starts when your computer boots up and manages access to memory, the CPU (threads/processes), disk drives, hardware and networking. The kernel is responsible for providing security and ensuring that programs cannot interfere with each other. Separate programs can only communicate to each other through the kernel. The kernel is only a small part of the operating system: on my computer, the compressed Linux kernel is just 7.2 megabytes, the Windows 10 kernel is 10 megabytes and on macOS is 15 megabytes.

User space

User space refers to libraries, services, utilities, packages and programs that run outside the kernel. These services include the window manager (which manages most of the user interface), terminal/shell, scripting and utilities. The vast majority of your operating system is user-space: on my computer, user space takes up approximately 20 gigabytes on Linux and macOS, and 19 gigabytes on Windows 10 (i.e., roughly 2000 times larger than the kernel).

The user space communicates with the kernel and other programs using system calls. While the user-space functionality of your operating system is extensive, it communicates with the operating system kernel using a small number of system calls. There are approximately 350 system calls in Linux [1], each with a unique number [2]. For example, the system call number 257 opens a file [3]).

A container is then a feature of the operating system kernels to hide information and functionality from user-space programs or libraries. For example, the chroot system call can hide the existing filesystem and create a virtual filesystem. You could create a complete private copy of user-space in your home directory /home/devel/copy/. This alternative user space is active for a process after calling the chroot system. If the subprocess launches /usr/bin/ls to list files in a directory, the request will be redirected to launch /home/devel/copy/usr/bin/ls from the private user space. Linux allows containers to have restricted filesystems (via chroot) and shared but restricted networks, CPU, memory, devices and processes (via cgroups and unshare).

Containers work because user-space code is the vast majority of an operating system and has the greatest relevance to the end-user experience. Most Linux and Android distributions share a nearly identical kernel: their wildly different user spaces make them distinctive.

Therefore, a container is:

  • a copy of the user-space environment (this could be the gigabytes of data used in full operating system distribution, or just a bare minimum needed to get a program to run)

  • that runs using isolation features of the kernel.

Because containers do not require a separate kernel to be started and do not need to run inside a virtual machine, they have many benefits:

  • They do not use as much CPU resources as a virtual machine

  • They do not experience the poor performance sometimes caused by virtualization

  • They can be started and stopped very fast

  • They can easily share resources with other containers (e.g., filesystems, network ports, IP addresses)

  • They can be easily managed as an ordinary part of the filesystem, rather than in specialized disk images

Docker

Docker is so well-known as to be synonymous with containerization. Docker provides a custom programming language to build containers and manages the storage and tracking of user-space filesystems.

Warning
You can install Docker on Windows and macOS. However, Linux containers running on Docker in Windows or macOS use virtualization, rather than containerization. Thus, a Linux container on Windows/macOS does not have the flexibility or perform as well as a Linux container running on Linux.

You can define a Docker image with Dockerfile. An example follows.

# Start with the latest dockerhub image for Ubuntu 20.04
FROM ubuntu:20.04

# Update the Ubuntu package database
RUN apt-get update

# Install Node.js with the default package manager
# noninteractive is used to avoid asking to set the timezone
RUN DEBIAN_FRONTEND=noninteractive apt-get -y install npm

# Copy the Node.js project into the container
COPY helloworld/ /helloworld/

# Set the default command for this Docker container
CMD node /helloworld/index.js

Each command in the Dockerfile corresponds to the following steps:

  1. Downloads the Ubuntu 20.04 image and uses this as the base of the image

  2. Updates the package manager database

  3. Installs Node.js using the Ubuntu package manager

  4. Copies the contents of the helloworld/ directory into /helloworld/ in the container

  5. Sets the default command of the container to run the Node.js script

Note that the RUN commands are ordinary terminal commands that you would use as an administrator to upgrade a regular Ubuntu installation. A Dockerfile is a script of terminal commands that build an user-space disk image.

The Dockerfile can be built by changing to the directory and running the docker build command:

$ cd chapter15_docker/ubuntu_node/
$ docker build -t ubuntu_node .
...
Successfully built e4ad8ecaec6b
Successfully tagged ubuntu_node:latest
$

The -t ubuntu_node parameter gives the newly created Docker image a name.

You can now run this this image as a container:

$ docker run --rm ubuntu_node
Hello world, built from ubuntu:20.04
$

The --rm parameter tells Docker to clean up all the temporary files created by the running container, once the container exits.

Docker Hub contains many pre-built images for containers. These can be selected simply by changing the FROM command in a Dockerfile. For example, there are pre-built Docker images for Node.js:

# Start with the latest Dockerhub image for Node.js
# This image is built on Debian Linux
FROM node:latest

# Copy the Node.js project into the container
COPY helloworld/ /helloworld/

# Set the default command for this Docker container
CMD node /helloworld/index.js

The docker run command has many other flags and parameters. Most importantly:

  • -u, which runs the container as a specific user (rather than as the system administrator). For example, docker run --rm -u $(id -u):$(id -g) ubuntu_node will run the container as the current user.

  • -p, which exposes a port on the container to the host. For example, docker run --rm -p 3000:3000 ubuntu_node will expose an Express application running on port 3000.

  • -v, which gives a container access to the host’s filesystem. For example docker run --rm -v $(pwd):/cwd ubuntu_node will allow the current working directory to be visible to the container inside the /cwd directory.

Benefits of Docker

Docker allows every aspect of a system to be specified. A Dockerfile specifies complete installation instructions for a user-space environment. These instructions avoid the “dependency hell” that can occur: wondering whether the server has the correct version of every library and all the required dependencies. In a team, a Dockerfile can ensure that every developer can build and use a replica of the production environment, from their laptop.

Furthermore, Docker Compose allows you to specify entire clusters of services. A single Docker Compose file can deploy a cluster of servers, correctly installed with the latest operating systems, with the correct version of the code configured and the appropriate services running.

Exercise: Create a Dockerfile

Create a Dockerfile for an application you have been developing.

Tip
Instead of using COPY, you might use the RUN command to perform a git pull and npm install.
Tip
To perform HTTP requests to the server, you will need to expose a network port from the container.
Tip
Because Docker will clean up all files created by your container, you must store databases in a shared directory (configured with the -v during docker run) or use an external database hosted outside Docker or in the cloud.

Alternatives to Docker

Docker is simply a friendly interface to the kernel’s containerization features.

A container can be created on Linux without any special software by copying a user-space filesystem and running standard Linux commands (cgcreate, cgexec, unshare and chroot).

While Docker has become synonymous with containerization, there are many popular alternatives to Docker and Docker Compose:


1. You can see the listing by entering man syscall in a Linux terminal.
2. The system call numbers are typically defined in /usr/include/asm/unistd_64.h or /usr/include/x86_64-linux-gnu/asm/unistd_64.h on AMD64 Linux systems.
3. The glibc open function uses the openat (257) system call.
4. Podman can be a drop-in replacement for Docker. Its command-line interface on Linux is very similar to Docker.
5. Kubernetes is a powerful platform designed to manage large clusters of containers.