containerd[1] es, probablemente, el runtime de contenedores más extendido del planeta, y al mismo tiempo uno de los menos conocidos. Cuando Kubernetes 1.24 consumó la retirada del dockershim en mayo de 2022, miles de clusters acabaron usando containerd como runtime por defecto. Pocos operadores se enteraron.

Esa es precisamente la señal de que la migración salió bien: un runtime bien integrado tiene que ser invisible. Este artículo lo hace visible el tiempo suficiente para entender qué corre debajo de cada pod.

Puntos clave

  • containerd gestiona el ciclo de vida completo de un contenedor en un nodo: descargar imagen, crear, arrancar, pausar, parar, montar filesystem y coordinar red con el CNI.

  • La pila en capas (Kubernetes → CRI → containerd → OCI → runc) permite sustituir piezas sin romper nada arriba.

  • Docker ha usado containerd internamente desde 2017; el dockershim era solo un traductor que Kubernetes 1.24 eliminó.

  • containerd domina en cloud (EKS, GKE, AKS); CRI-O es estándar en OpenShift.

  • crictl, ctr y nerdctl son las tres herramientas para operar containerd directamente cuando kubectl no es suficiente.

Qué es containerd y qué no

containerd es un runtime de contenedores de alto nivel. Su responsabilidad es el ciclo de vida completo de un contenedor en una máquina. Eso es descargar la imagen desde un registro, almacenarla, crear el contenedor, arrancarlo, pausarlo, pararlo, montar snapshots del filesystem y coordinar los namespaces de red con el CNI del nodo. Todo esto como demonio, sin interfaz gráfica ni experiencia de desarrollador.

Lo que no hace: no construye imágenes (no conoce el Dockerfile) ni orquesta, que es tarea de Kubernetes por encima. Tampoco compone conjuntos de contenedores ni pretende sustituir a una herramienta interactiva tipo Docker CLI.

containerd nació como componente interno de Docker Engine. En marzo de 2017 fue donado a la CNCF; en febrero de 2019 alcanzó la categoría de proyecto graduado[2], el quinto tras Kubernetes, Prometheus, Envoy y CoreDNS. Desde entonces se ha convertido en la base común sobre la que construyen Docker, Kubernetes, BuildKit y el resto del ecosistema OCI.

La pila en capas

Entender containerd requiere ver la pila completa:

Kubernetes (orquestación)
    │
    └── CRI (Container Runtime Interface) — gRPC API
            │
            ▼
        containerd (runtime de alto nivel)
            │
            └── OCI spec → runc (o crun, gVisor, Kata Containers)
                                │
                                └── kernel Linux (namespaces, cgroups, seccomp)

Esta separación permite sustituir piezas sin romper nada más arriba. Puedes cambiar runc por crun (reimplementación en C), por gVisor (sandbox en espacio de usuario para aislamiento fuerte) o por Kata Containers (microVM por contenedor), y Kubernetes no se entera. Puedes cambiar containerd por CRI-O con poco más que un flag de kubelet.

CRI y OCI son los dos contratos que hacen posible ese desacoplamiento.

Dos conceptos internos importantes. Los namespaces de containerd (distintos de los del kernel) son espacios lógicos que separan imágenes y contenedores de distintos consumidores: Kubernetes usa k8s.io, Docker usa moby. Consultar el demonio sin indicar namespace hace que parezca vacío aunque el nodo esté lleno. Los snapshotters montan el filesystem con overlays: el por defecto es overlayfs, con alternativas (btrfs, zfs, stargz para lazy pulling) configurables en el daemon.

containerd y Docker: la relación real

La verdad es que containerd y Docker llevan años siendo la misma cosa por debajo. Docker Engine usa containerd internamente desde 2017: cuando lanzas docker run, Docker construye la spec OCI, llama a containerd por un socket local y containerd invoca runc. El comando es una abstracción conveniente sobre el runtime real.

Por eso los clusters Kubernetes que decían "usar Docker" ya ejecutaban sus contenedores con containerd; el dockershim era un traductor entre CRI y la API de Docker Engine. La retirada del dockershim en Kubernetes 1.24[3] eliminó esa capa de traducción. Kubernetes habla ahora directamente con containerd (o con CRI-O), sin pasar por Docker Engine.

Para un cluster, el cambio fue reconfigurar kubelet y reiniciar nodos. Para un servidor de desarrollo con Docker, nada cambia.

containerd frente a CRI-O

CRI-O[4] es la alternativa más relevante. Nació en Red Hat con un objetivo explícito: un runtime mínimo construido exclusivamente para Kubernetes, sin nada que CRI no requiera.

  • containerd: más general, lo usa Docker, lo usan entornos de desarrollo, lo usa BuildKit y lo usa Kubernetes.

  • CRI-O: más específico, hecho solo para Kubernetes, sin nada extra.

Ambos implementan CRI y OCI, ambos usan runc por defecto y el rendimiento es comparable en benchmarks serios. Ambos son además proyectos CNCF graduados: CRI-O alcanzó esa categoría en julio de 2023[5], cuatro años después que containerd. La elección es más de ecosistema y soporte que de capacidades: containerd domina en upstream Kubernetes y en cloud (EKS, GKE, AKS); CRI-O es estándar en OpenShift y el ecosistema Red Hat.

Operar containerd directamente

En el día a día con Kubernetes no se toca containerd: kubelet se encarga. Cuando algo se rompe en un nodo y kubectl describe no da pistas suficientes, tres herramientas son esenciales:

  • ctr: la CLI nativa de containerd. Cruda y poco amigable, pero permite ver exactamente lo que el demonio tiene.

  • crictl: la herramienta oficial de Kubernetes para depurar runtimes CRI. Habla con containerd por el socket CRI (unix:///run/containerd/containerd.sock) y ofrece subcomandos orientados a pods y contenedores. Configuración en /etc/crictl.yaml.

  • nerdctl[6]: CLI compatible con Docker que ataca containerd directamente. Ver el artículo completo en nerdctl-alternativa-docker.

# Debug típico desde un nodo Kubernetes con containerd
sudo crictl pods --namespace kube-system
sudo crictl ps -a
sudo crictl logs --tail 200 <container-id>
sudo crictl exec -it <container-id> sh
sudo crictl inspecti registry.k8s.io/pause:3.9

# Si echas de menos la sintaxis Docker
sudo nerdctl --namespace k8s.io ps
sudo nerdctl --namespace k8s.io images

# Para ver el estado crudo del demonio
sudo journalctl -u containerd -f

Los cuatro casos donde esto compensa son concretos:

  • Pod que no arranca y cuyos logs de kubelet no aclaran el motivo: containerd suele tener el detalle en journalctl -u containerd.

  • Nodo con disco lleno que necesita crictl rmi --prune porque el GC de Kubernetes va lento.

  • ImagePullBackOff que se resuelve haciendo crictl pull directo para aislar si el problema es de credenciales o de red.

  • Auditar qué runtime corre cada nodo con kubectl get nodes -o wide.

Configuración habitual

El fichero principal vive en /etc/containerd/config.toml. Los cambios más habituales:

  • Mirrors de registro: para no saturar Docker Hub ni depender de él en producción.

  • Runtimes alternativos: configurar gVisor o Kata Containers para cargas sensibles.

  • Snapshotter: elegir btrfs o zfs si el entorno lo justifica.

  • Cgroup driver a systemd: alinear con kubelet. Este es el error clásico de instalación: kubelet con systemd y containerd con cgroupfs produce reinicios aleatorios de pods difíciles de diagnosticar.

Cualquier cambio en la configuración requiere systemctl restart containerd y, en producción, hacerlo nodo por nodo con cordon y drain. Los patrones de observabilidad de nodo de los que depende el diagnóstico de incidentes en containerd se integran bien con el stack de observabilidad con Grafana.

Conclusión

containerd es la pieza silenciosa sobre la que corre la mayor parte de Kubernetes. Entender su arquitectura no es un capricho académico. Es lo que convierte un incidente opaco en uno diagnosticable, y lo que permite decidir con criterio sobre runtimes alternativos cuando la seguridad o el aislamiento lo piden. También lo que te deja leer notas de lanzamiento que hablan de CRI, snapshotters o shims.

En operación ordinaria seguirá siendo una capa invisible; el día que toque bajar, hay que saberse el camino.

Este artículo también está disponible en inglés: containerd: The Runtime Underpinning Kubernetes.

Fuentes:

  1. containerd: sitio oficial del proyecto[1]
  2. CRI-O: sitio oficial del proyecto[4]
  3. Kubernetes: Dockershim Deprecation FAQ[3]
  4. CNCF: Cloud Native Computing Foundation Announces containerd Graduation[2]
  5. CNCF: Cloud Native Computing Foundation Announces Graduation of CRI-O[5]
  6. nerdctl: repositorio en GitHub[6]

Preguntas frecuentes

¿Por qué `ctr` o `nerdctl` no muestran ningún contenedor aunque el nodo esté lleno de pods?

Porque containerd separa imágenes y contenedores en namespaces lógicos (distintos de los del kernel) según el consumidor: Kubernetes usa k8s.io y Docker usa moby. Si consultas el demonio sin indicar namespace, parece vacío. Usa sudo nerdctl --namespace k8s.io ps o sudo nerdctl --namespace k8s.io images para ver lo que ejecuta kubelet, o crictl, que habla con containerd por el socket CRI unix:///run/containerd/containerd.sock y ya está orientado a pods y contenedores de Kubernetes.

¿Qué pasa si kubelet usa el cgroup driver systemd y containerd usa cgroupfs?

Es el error clásico de instalación: esa desalineación produce reinicios aleatorios de pods difíciles de diagnosticar. El cgroup driver se ajusta en /etc/containerd/config.toml, el mismo fichero donde se configuran mirrors de registro, runtimes alternativos como gVisor o Kata Containers y el snapshotter (overlayfs por defecto, con btrfs o zfs como alternativas). Cualquier cambio requiere systemctl restart containerd y, en producción, hacerlo nodo por nodo con cordon y drain.

¿Tuve que cambiar algo en mis servidores con Docker cuando Kubernetes 1.24 eliminó el dockershim?

En un servidor de desarrollo con Docker, nada: Docker Engine usa containerd internamente desde 2017, así que docker run construye la spec OCI, llama a containerd por un socket local y containerd invoca runc. El dockershim era solo un traductor entre CRI y la API de Docker Engine; al retirarlo en mayo de 2022, Kubernetes pasó a hablar directamente con containerd o CRI-O. Para un cluster, el cambio consistió en reconfigurar kubelet y reiniciar nodos; puedes auditar qué runtime corre cada nodo con kubectl get nodes -o wide.

Fuentes

  1. containerd
  2. alcanzó la categoría de proyecto graduado
  3. retirada del dockershim en Kubernetes 1.24
  4. CRI-O
  5. alcanzó esa categoría en julio de 2023
  6. nerdctl