containerd doesn’t actually manage persistent storage for your workloads; it delegates that responsibility to the underlying operating system’s storage drivers and the container runtime’s configuration.

Let’s see this in action. Imagine you’re running a PostgreSQL database in a container. You want its data to survive container restarts and even node reboots. This means the data needs to live outside the container’s ephemeral filesystem.

Here’s a typical containerd configuration snippet for a storage driver, usually found in /etc/containerd/config.toml:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
  SystemdCgroup = true
  IoGid = 0
  IoUid = 0
  NoNewKeyring = false
  NoPivotRoot = false
  Root = "/var/lib/containerd/rootfs"
  ShimCgroup = ""
  ShimPidFile = ""
  Cgroup = ""
  CgroupParent = ""
  IgnoreExitCode = false
  NoNewPrivileges = false
  Concurrency = 0
  OciSpec = ""
  CgroupDriver = "systemd" # This is a key setting!

The CgroupDriver here, typically set to systemd, tells containerd (via the CRI plugin) how to manage the container’s resources and isolation. It doesn’t directly dictate where your persistent data goes. That’s handled by Kubernetes (if you’re using it) or directly by how you mount volumes into your container.

The real magic for persistent storage happens at the Kubernetes level (or whatever orchestrator you’re using). When you define a PersistentVolumeClaim (PVC) and PersistentVolume (PV), you’re telling the system to allocate storage that exists independently of the Pod’s lifecycle.

A PVC might look like this:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: standard # This points to a StorageClass

And a StorageClass defines how that storage is provisioned. For example, using hostPath (not recommended for production!) or a cloud provider’s block storage.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
provisioner: kubernetes.io/no-provisioner # For manual PV provisioning
# Or for a cloud provider:
# provisioner: kubernetes.io/aws-ebs
# parameters:
#   type: gp2
#   fsType: ext4

When a Pod uses this PVC, Kubernetes ensures that a corresponding volume is mounted into the container. containerd then receives instructions from the CRI to mount this specific host path or device into the container’s filesystem at a specified path.

For instance, if your Pod definition includes:

spec:
  containers:
  - name: postgres
    image: postgres:14
    ports:
    - containerPort: 5432
    volumeMounts:
    - name: postgres-storage
      mountPath: /var/lib/postgresql/data
  volumes:
  - name: postgres-storage
    persistentVolumeClaim:
      claimName: postgres-data-pvc

containerd will, upon receiving the Pod’s spec from the Kubelet, ensure that the volume specified by postgres-data-pvc (which Kubernetes has bound to a real PV) is mounted at /var/lib/postgresql/data inside the container. The actual storage backend (e.g., an EBS volume, an NFS share, or even a directory on the host’s disk) is managed by the volume provisioner and Kubernetes, not directly by containerd’s storage driver configuration.

The containerd storage driver, like overlayfs or aufs, primarily deals with the ephemeral layers of the container’s root filesystem. It’s about how the container image is unpacked and layered, not about the persistent data volumes you attach.

The most surprising true thing about containerd’s storage backend configuration is that it’s largely irrelevant to persistent data. The [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] section, especially Root and CgroupDriver, configures the container runtime’s view of the filesystem and its resource isolation, but the actual persistent data resides on volumes managed outside of containerd’s core image/layering mechanisms.

If you’re using containerd directly without Kubernetes, you’d be responsible for manually creating and mounting storage volumes into your containers when you run them using ctr or another tool. The concept remains the same: containerd facilitates the execution of the container, but the lifecycle and persistence of attached storage are handled by external mechanisms or explicit manual configuration.

The next logical step is understanding how to configure containerd to use different image storage drivers, which are managed within config.toml and impact how container images are stored and accessed on disk, affecting pull times and disk usage.

Want structured learning?

Take the full Containerd course →