Lesson  in  How (and Why) to Use containerd from the Command Line

How to Install and Configure containerd on a Linux Server

Downloading and putting together the main components of a containerd installation.

Why this lesson?

Earlier in the course, we learned how to pull images and run containers using ctr, the default containerd command-line client. In our experiments, we relied on an already running containerd daemon that comes with every Docker Engine installation. However, while convenient, this approach doesn't fully reveal the underlying components. Yes, there is a daemon (containerd) exposing an API and a client (ctr), but what else makes up a functional containerd setup?

In this lesson, we will install containerd manually by downloading all the necessary components and placing them in the correct locations on the system. This hands-on approach will give us a clearer picture of the moving parts (fortunately, there are only a few) and help us understand the nerdctl architecture when we use it to interact with containerd in the next lesson.

Note

For the containerd instance that comes with Docker Engine, look for the containerd.io package in Debian, Ubuntu, Fedora, or other Linux installation instructions.

Main containerd components

One of the main installation options mentioned in the official Getting started with containerd guide is to download it from the project's GitHub Releases page.

To get a containerd release archive for a specific version and architecture, use the following command:

VERSION=2.4.1
ARCH=amd64
URL=https://github.com/containerd/containerd/releases/download/v${VERSION}/containerd-${VERSION}-linux-${ARCH}.tar.gz

curl -fLO ${URL}

Every release archive comes with a .sha256sum file. Download it too and verify the archive before extracting it:

curl -fLO ${URL}.sha256sum

sha256sum -c containerd-${VERSION}-linux-${ARCH}.tar.gz.sha256sum
containerd-2.4.1-linux-amd64.tar.gz: OK

What's inside the containerd 2.x archive? Extract the archive to inspect its contents:

TMP_DIR=$(mktemp -d)
tar Cxzvf ${TMP_DIR} containerd-${VERSION}-linux-${ARCH}.tar.gz
bin/
bin/containerd
bin/containerd-shim-runc-v2
bin/containerd-stress
bin/ctr

Interestingly, aside from the stress test tool, the archive contains only three core components:

And that's it!

To "install" containerd, move the binaries from the release archive to /usr/local/bin (or any other directory in your $PATH):

sudo tar Cxzvf /usr/local \
    containerd-${VERSION}-linux-${ARCH}.tar.gz
Note

containerd 1.x vs. 2.x: The major version bump was primarily due to the removal of deprecated components, including (but not limited to) containerd-shim and containerd-shim-runc-v1 binaries. Thus, the containerd 1.x release archive may have a few extra files in it.

Aside from these removals, containerd 2.x is more of an evolutionary update rather than a revolutionary change.

Tip

LTS vs. regular releases: Starting with containerd 2.3, a new minor version comes out about every 4 months, following the Kubernetes release schedule. One minor release per year is a Long Term Stable (LTS) release with at least two years of support. The first one is 2.3, while 2.4 is a regular release with a shorter support window. This lesson uses the latest release, but for production servers, the latest 2.3.x LTS release may be a safer choice.

Important

Note that the containerd binaries in the archive above are dynamically linked against glibc (version 2.35 or newer, as in Ubuntu 22.04). If you need to run containerd on a musl-based Linux distribution or on a system with an older glibc, download the containerd-static-<VERSION>-linux-<ARCH>.tar.gz variant of the release archive.

containerd as a systemd service

With the binaries installed, you can run containerd with a simple:

sudo containerd

However, usually you will want to run containerd as a systemd service. Luckily, the containerd project provides a systemd unit file, and it's a relatively straightforward one.

containerd.service systemd unit file

The file below is provided as an example. Make sure to download the unit file from the containerd repository instead of copying it from here!

# Copyright The containerd Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target dbus.service

[Service]
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/containerd

Type=notify
Delegate=yes
KillMode=process
Restart=always
RestartSec=5

# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNPROC=infinity
LimitCORE=infinity

# Comment TasksMax if your systemd version does not supports it.
# Only systemd 226 and above support this version.
TasksMax=infinity
OOMScoreAdjust=-999

[Install]
WantedBy=multi-user.target

Download the unit file from the same release tag as the binaries and put it into /usr/local/lib/systemd/system:

sudo mkdir -p /usr/local/lib/systemd/system

sudo curl -fL \
    https://raw.githubusercontent.com/containerd/containerd/v2.4.1/containerd.service \
    -o /usr/local/lib/systemd/system/containerd.service

Reload the systemd daemon and enable the containerd service:

sudo systemctl daemon-reload
sudo systemctl enable --now containerd

You can check the status of the containerd service with:

sudo systemctl status containerd
Tip

By default, the containerd service works without any configuration files. However, if you need to customize it, you can generate a default configuration file:

sudo mkdir -p /etc/containerd

containerd config default | sudo tee /etc/containerd/config.toml

... and then edit it as required. Don't forget to restart the containerd service after that.

The generated file starts with version = 4. This is the configuration format version, and it's not the same as the containerd version. containerd 2.x also reads older configuration files (version = 2 and version = 3) and migrates them in memory on every start. The containerd config migrate command converts such a file to the current format once and for all.

Preliminary containerd testing

Let's test the containerd installation by running a container. Unlike Docker, containerd expects you to explicitly pull the image first:

sudo ctr image pull ghcr.io/iximiuz/labs/nginx:alpine

So far so good! When the image is pulled, you can run a container:

sudo ctr run ghcr.io/iximiuz/labs/nginx:alpine nginx1

Oops! Trying to run a container will likely fail with the following error:

ctr: failed to create shim task: OCI runtime create failed:
unable to retrieve OCI runtime error (open /run/containerd/.../nginx1/log.json: no such file or directory):
exec: "runc": executable file not found in $PATH

However, in hindsight, this should be rather expected. We haven't installed runc or any other container runtime yet, and containerd cannot run containers on its own (surprise, surprise). At the same time, containerd itself seems to be installed successfully - we could pull the image, and other commands that don't require running containers will likely also work fine.

What is a container runtime (runc)?

runc is a container runtime and a reference implementation of the OCI (Open Container Initiative) Runtime Specification.

To put it simply, runc is a command-line tool that knows how to create, start, stop, and delete containers given a container configuration and a root filesystem.

A typical OCI container runtime (runc) workflow.

Docker (through containerd), Podman, Kubernetes, and other "higher-level" container runtimes and orchestrators under the hood rely on runc (or an alternative OCI Runtime implementation) to run containers.

You can practice using runc in the Create and Start a Container Manually With runc challenge.

Adding OCI container runtime(s)

The containerd release archive doesn't include runc or any other OCI container runtimes. You will need to install them separately. Luckily, runc installation is rather trivial - it's just a single statically linked binary, which you can download from the project's GitHub Releases page:

VERSION=1.5.1
ARCH=amd64

curl -fLO https://github.com/opencontainers/runc/releases/download/v${VERSION}/runc.${ARCH}

The checksums of all runc binaries are published in a single (GPG-signed) runc.sha256sum file. The --ignore-missing flag makes sha256sum check only the files present in the current directory:

curl -fLO https://github.com/opencontainers/runc/releases/download/v${VERSION}/runc.sha256sum

sha256sum -c --ignore-missing runc.sha256sum
sha256sum: WARNING: 9 lines are improperly formatted
runc.amd64: OK

The warning is about the lines of the GPG signature wrapped around the checksums, and it's safe to ignore.

Install the binary as /usr/local/sbin/runc and make it executable in one go:

sudo install -m 755 runc.${ARCH} /usr/local/sbin/runc

Now, try running a container once again:

sudo ctr run ghcr.io/iximiuz/labs/nginx:alpine nginx2

Huge success! Keep this container running for a while - we'll need it in the next unit.

Notice how the OCI runtime was not included in the containerd release archive, whereas the runtime shim was. Runtimes are designed to be replaceable and interchangeable, and the shim acts as a "glue" between the container runtime and the containerd daemon, shielding the latter from any peculiarities of a specific runtime implementation.

Layered containerd architecture: client(s), daemon, runtime shim(s), and container runtime(s).
Note

Unlike the GitHub release archive, the containerd.io package for Debian, Ubuntu, Fedora, and other Linux distributions includes runc. So, if you install containerd via the package manager, you don't need to install runc separately.

Testing container networking

We just successfully started an Nginx container. But can we send a request to it?

Docker containers are usually accessible from the host machine via their IP addresses. However, if you try to list the network addresses of the nginx2 container (from a separate terminal tab), you'll only see the loopback interface:

sudo ctr t exec --exec-id ip nginx2 \
    ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever

You can still send a request to the Nginx server from inside the container:

sudo ctr t exec --exec-id curl nginx2 \
    curl -s -X GET http://localhost:80

...but there is no way to access the Nginx server from the host machine via an IP address.

This is because by default, the ctr run command doesn't configure any external network interfaces for the container. To enable the de facto standard bridge container networking, you need to use the --cni flag:

sudo ctr run --cni \
    ghcr.io/iximiuz/labs/nginx:alpine nginx3

However, the above command will likely fail with the following error:

ctr: no network config found in /etc/cni/net.d: cni plugin not initialized

This happens because the containerd release archive from GitHub doesn't include the CNI plugins.

Installing and configuring CNI plugins

Container Network Interface (CNI) plugins are statically linked executables that are used to configure the network devices and other network-related resources for containers.

To install the CNI plugins, download the archive from the project's GitHub Releases page:

VERSION=1.9.1
ARCH=amd64
URL=https://github.com/containernetworking/plugins/releases/download/v${VERSION}/cni-plugins-linux-${ARCH}-v${VERSION}.tgz

curl -fLO ${URL}

Verify the archive using the .sha256 file published next to it:

curl -fLO ${URL}.sha256

sha256sum -c cni-plugins-linux-${ARCH}-v${VERSION}.tgz.sha256

...and extract it to the /opt/cni/bin directory (the expected location for CNI plugins on Linux):

sudo mkdir -p /opt/cni/bin

sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-${ARCH}-v${VERSION}.tgz
./
./loopback
./bridge
./host-local
...and a dozen of other binaries

The CNI bundle includes more than a dozen plugins, with bridge, host-local IPAM, and loopback being the most relevant ones for our use case:

  • bridge - Creates a bridge, adds the host and the container to it.
  • host-local IPAM - Maintains a local database of allocated IPs.
  • loopback - Sets the state of the container's loopback interface to up.

As the ctr run --cni error message from the previous section indicated, the CNI network configuration files are expected to be present in the /etc/cni/net.d directory.

A network configuration file describes a chain of plugins to call for every container. For a bridge network, the chain has just one plugin, bridge, which in turn delegates the IP address management (IPAM) to the host-local plugin. The configuration needs the bridge device name, the IP address range for the bridge network, and the default gateway route for containers:

sudo mkdir -p /etc/cni/net.d

cat <<EOF | sudo tee /etc/cni/net.d/10-bridge.conflist
{
  "cniVersion": "1.1.0",
  "name": "bridge",
  "plugins": [
    {
      "type": "bridge",
      "bridge": "bridge0",
      "isGateway": true,
      "ipMasq": true,
      "ipam": {
        "type": "host-local",
        "ranges": [
          [{"subnet": "172.18.0.0/24", "gateway": "172.18.0.1"}]
        ],
        "routes": [{"dst": "0.0.0.0/0"}]
      }
    }
  ]
}
EOF
Note

The .conflist file above follows the network configuration format of the current CNI specification. You may also see older .conf files that hold a single plugin's configuration at the top level (without the plugins list). containerd still accepts them, but the .conflist format is what newer tools, such as nerdctl, generate.

Important

ctr run --cni loads only the first file from /etc/cni/net.d, sorted by name. This is why CNI configuration files usually have numeric prefixes (10-, 20-, ...). Any other files in the directory are ignored by ctr.

There is no need for a separate loopback network configuration either. When runc creates a new network namespace for the container, it brings the lo interface up by itself. The loopback plugin binary is still worth keeping in /opt/cni/bin: containerd's CRI plugin (the one Kubernetes talks to) calls it for every pod.

There is no need to restart the containerd service after the CNI plugins are installed because it will just try executing the CNI binaries at a well-known location on every ctr run --cni command.

Now, you can run another Nginx container, this time with an external network interface:

sudo ctr run --cni \
    ghcr.io/iximiuz/labs/nginx:alpine nginx4

Testing time (from a separate terminal tab):

# Guessing the IP address here as the first available one in the
# 172.18.0.0/24 subnet, reserving 172.18.0.1 for the bridge itself.
curl -s http://172.18.0.2:80
Note

CNI plugins are optional (we were able to run the Nginx container without them), and Docker's version of containerd (the containerd.io package) doesn't include them. However, if you want to use containerd as a self-sufficient container runtime, potentially via nerdctl or as a CRI runtime in your Kubernetes cluster, you'll have to install and configure the CNI plugins.

Summary

If you followed this hands-on lesson, you should have a solid understanding of the main moving parts of a containerd installation.

The containerd release archive includes only the essential components:

  • ctr - the command-line client
  • containerd - the daemon itself
  • containerd-shim-runc-v2 - an OCI container runtime shim

These components are enough to start the containerd daemon and even pull some images, but to run containers, you'll also need to install (and configure):

  • A container runtime (e.g. runc)
  • A set of CNI plugins (e.g. bridge, host-local, loopback)

Visually, a containerd installation can be represented as follows:

Main components of a containerd installation: ctr, containerd, containerd-shim, runc, and CNI plugins.

Notice how containerd supports different container runtimes and CNI plugins and can be used via various command-line and programming clients. It's like LEGO bricks all the way down (and up)!

In the next lesson, we'll explore how to extend a containerd installation with nerdctl, a Docker-compatible CLI client. But first - practice time!

Practice

Can you install and configure containerd by yourself?