Skip to content

Containers

Fundamentals

Installed containerd — but understanding what a container is and why containerd exists is critical before for Homelab Project's Phase 2.

Theory

A container is not a VM. A VM runs a full guest OS with its own kernel on emulated hardware. A container is just a Linux process with two kernel features applied:

  • Namespaces — isolate what a process can see (its own PID tree, network stack, mount points, hostname, users). The process thinks it's alone on the machine.
  • Cgroups (control groups) — limit what a process can use (CPU, RAM, disk I/O, network bandwidth). The kernel enforces hard limits.

That's it. Containers share the host kernel. This is why they start in milliseconds (no OS to boot) and why foundational kernel tuning directly affects container behavior.

The OCI standard. The Open Container Initiative defines two specs:

  • Image spec — how container images are packaged (layers, manifests, configs)
  • Runtime spec — how containers are created and run (runc is the reference implementation)

Any tool that follows these specs is interchangeable. This is why you can build images with Docker and run them with containerd.

Container runtimes — the stack:

Kubernetes (kubelet)
    ▼ CRI (Container Runtime Interface)
containerd
    ▼ OCI Runtime
runc
    ▼ Linux Kernel
namespaces + cgroups
  • kubelet talks to the container runtime via the CRI (gRPC API).
  • containerd manages image pulling, storage, and container lifecycle.
  • runc does the actual low-level work of creating namespaces and cgroups.
  • Docker Desktop includes containerd inside it but adds a CLI, a daemon, and Docker Compose — layers Kubernetes doesn't need. That's why K8s dropped dockershim in v1.24.

Cgroups v1 vs v2 and the systemd driver. Cgroups come in two versions. Debian 13 uses cgroups v2 by default. There are two ways to manage the cgroup hierarchy:

Driver How it works
cgroupfs The container runtime directly writes to the cgroup filesystem.
systemd The runtime delegates cgroup management to systemd, which already manages the system's cgroup tree.

If kubelet uses systemd but containerd uses cgroupfs, they fight over the cgroup tree. Both must agree. That's why you set SystemdCgroup = true in containerd's config — to match what kubelet expects on a systemd-based distro.

Obstacles

  • "Why not just install Docker?" — You can, but you'd be installing Docker → which bundles containerd → which talks to runc. Kubernetes skips Docker and talks to containerd directly. Installing Docker adds an unnecessary layer.
  • containerd's default config sets SystemdCgroup = false. You have to generate the config with containerd config default and flip the flag. Missing this causes kubelet to fail on init with cryptic cgroup errors.
  • crictl vs ctr vs docker. crictl talks to the CRI socket (what kubelet uses). ctr talks to containerd directly (lower level). docker talks to the Docker daemon. For K8s troubleshooting, use crictl.

Implementation

  • ansible/playbooks/01-install-containerd.yaml — automates containerd installation and guarantees SystemdCgroup = true is set declaratively via template.

Resources