27 серпня 2026 р.
Containerd замість Docker

Фраза «containerd замість Docker» часто звучить так, ніби достатньо видалити один пакет і встановити інший. Насправді Docker Engine і containerd перекривають лише частину функцій. Docker дає користувацький CLI, build, network, volumes, Compose та daemon API. Containerd зосереджений на життєвому циклі контейнерів, images, snapshots і взаємодії з OCI runtime. У Kubernetes kubelet підключається до нього через Container Runtime Interface, тому Docker daemon на worker node стає непотрібним.
Це не означає, що Docker потрібно прибрати з усіх середовищ. Розробник може й далі виконувати `docker build`, а CI — збирати image через BuildKit. Готовий OCI image публікується в registry, після чого Kubernetes node завантажує його через containerd. Локальне сховище images Docker і containerd не спільне: образ, зібраний командою `docker build` на node, не з’явиться автоматично в CRI.
У цій інструкції встановимо containerd 2.3 LTS на Ubuntu, налаштуємо CRI та systemd cgroups, додамо `crictl`, перевіримо runtime без Kubernetes і розберемо міграцію існуючого node з Docker Engine. Для production-кластера такі зміни потрібно виконувати послідовно, з перевіркою кожного вузла та готовим rollback. Якщо платформа обслуговує критичні workloads, runtime lifecycle варто включити до DevOps-супроводу інфраструктури, а не залишати ручною процедурою без журналу змін.
Чим containerd відрізняється від Docker Engine
Containerd керує завантаженням і зберіганням images, створенням container lifecycle, snapshots та викликом низькорівневого OCI runtime, найчастіше `runc`. Для Kubernetes важлива CRI-служба: kubelet надсилає запити через Unix socket `/run/containerd/containerd.sock`, а containerd створює Pod sandbox і containers згідно з desired state кластера.
Docker Engine сам використовує containerd усередині, але додає власний daemon, API, image store, build workflow і UX. Коли kubelet працює через `cri-dockerd`, між ним і Docker є окремий CRI adapter. При прямому containerd ця ланка зникає. Вбудований dockershim видалений із Kubernetes починаючи з 1.24, а сучасні версії Kubernetes вимагають CRI v1.
Після переходу команда `docker ps` більше не показує Kubernetes containers. Для штатних операцій використовуйте `kubectl`; для діагностики CRI на конкретному node — `crictl`. Команда `ctr` є низькорівневим debug-клієнтом containerd, а `nerdctl` надає Docker-подібний інтерфейс, але жоден із них не повинен обходити Kubernetes API для звичайного керування Pod.
Перед міграцією знайдіть залежності від Docker:
- privileged DaemonSet викликає `docker ps`, `docker inspect` або монтує `/var/run/docker.sock`; - monitoring очікує Docker-specific container names або daemon metrics; - security agent читає Docker API; - локальний pipeline збирає image без push у registry; - scripts перезапускають `docker.service` або редагують `/etc/docker/daemon.json`; - log collector використовує Docker-specific paths.
Такі залежності потрібно виправити до зміни runtime. Сам workload у стандартному OCI image зазвичай не потребує переписування.
Встановлення containerd 2.3 LTS на Ubuntu
На дату підготовки матеріалу containerd 2.3 є LTS-гілкою з підтримкою до квітня 2028 року. Не використовуйте beta 2.4 на production nodes. Нижче показано офіційний binary для `amd64`; для `arm64` змініть architecture та звірте release assets.
Спочатку перевірте систему, cgroup і поточний runtime:
uname -m
cat /etc/os-release
stat -fc %T /sys/fs/cgroup
systemctl is-active containerd || true
systemctl is-active docker || true
kubectl get nodes -o wide 2>/dev/null || trueВстановіть базові пакети та `runc`. Для production зафіксуйте версію runc із підтримуваної vendor repository або перевіреного офіційного release. Не змішуйте випадкові binaries з кількох джерел без compatibility test.
sudo apt-get update
sudo apt-get install -y ca-certificates curl runc
runc --versionЗавантажте containerd 2.3.3 і checksum з офіційного release, перевірте archive та розпакуйте binaries у `/usr/local`:
cd /tmp
export CONTAINERD_VERSION="2.3.3"
curl -fLO \
"https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VERSION}/containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz"
curl -fLO \
"https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VERSION}/containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz.sha256sum"
sha256sum -c \
"containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz.sha256sum"
sudo tar Cxzvf /usr/local \
"containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz"Встановіть systemd unit саме з того самого tag, потім увімкніть daemon:
sudo curl -fL \
"https://raw.githubusercontent.com/containerd/containerd/v${CONTAINERD_VERSION}/containerd.service" \
-o /usr/local/lib/systemd/system/containerd.service
sudo systemctl daemon-reload
sudo systemctl enable --now containerd
containerd --version
sudo systemctl status containerd --no-pagerДля Kubernetes network plugin зазвичай встановлює CNI-конфігурацію, але reference binaries мають бути в `/opt/cni/bin`. Якщо ваш обраний CNI installer не доставляє їх сам, встановіть офіційний пакет і checksum:
cd /tmp
export CNI_VERSION="v1.9.1"
curl -fLO \
"https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/cni-plugins-linux-amd64-${CNI_VERSION}.tgz"
curl -fLO \
"https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/cni-plugins-linux-amd64-${CNI_VERSION}.tgz.sha256"
sha256sum -c \
"cni-plugins-linux-amd64-${CNI_VERSION}.tgz.sha256"
sudo install -d -m 0755 /opt/cni/bin
sudo tar Cxzvf /opt/cni/bin \
"cni-plugins-linux-amd64-${CNI_VERSION}.tgz"Наявність binaries не створює мережу кластера. Calico, Cilium або інший CNI має окремо встановити конфігурацію та controllers відповідно до своєї документації.
CRI, systemd cgroups і crictl
Створіть повний default config саме встановленою версією containerd. Для containerd 2.x він використовує config version 3 та нові назви CRI plugins:
sudo install -d -m 0755 /etc/containerd
containerd config default | \
sudo tee /etc/containerd/config.toml >/dev/null
sudo cp /etc/containerd/config.toml \
/etc/containerd/config.toml.before-kubernetesВідкрийте `/etc/containerd/config.toml` і перевірте, що `cri` не міститься в `disabled_plugins`. Для containerd 2.x увімкніть systemd cgroup driver у секції runtime `runc`:
version = 3
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc]
runtime_type = 'io.containerd.runc.v2'
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
SystemdCgroup = trueНе копіюйте секцію для containerd 1.x у config 2.x: у старій гілці шлях починається з `io.containerd.grpc.v1.cri`, тому параметр у неправильній секції буде проігнорований. Cgroup driver kubelet і runtime повинні збігатися; для modern Ubuntu з cgroup v2 рекомендовано `systemd`.
Перезапустіть daemon і перевірте plugins:
sudo systemctl restart containerd
sudo systemctl --no-pager --full status containerd
sudo ctr plugins ls | grep -E 'cri|runtime|snapshot'
sudo journalctl -u containerd -n 100 --no-pagerВстановіть `crictl` версії, сумісної з minor-версією Kubernetes. Нижче `CRICTL_VERSION` навмисно є змінною: перед виконанням підставте release, який відповідає вашому cluster lifecycle.
cd /tmp
export CRICTL_VERSION="v1.36.0"
curl -fLO \
"https://github.com/kubernetes-sigs/cri-tools/releases/download/${CRICTL_VERSION}/crictl-${CRICTL_VERSION}-linux-amd64.tar.gz"
sudo tar Cxzvf /usr/local/bin \
"crictl-${CRICTL_VERSION}-linux-amd64.tar.gz"Зафіксуйте endpoint, щоб `crictl` не перебирав sockets і не показував warning:
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 10
debug: falseЗбережіть YAML як `/etc/crictl.yaml`, а потім перевірте CRI:
sudo crictl info
sudo crictl version
sudo crictl images
sudo crictl podsУ `crictl info` не повинно бути connection error. Також перевірте, що runtime condition має status `true`, а config показує systemd cgroups.
Міграція Kubernetes node з Docker на containerd
Міграцію виконуйте по одному worker node. Спочатку переконайтеся, що інші nodes мають достатньо CPU, RAM і storage для евакуації workloads, PodDisruptionBudget дозволяє drain, а stateful volumes можуть перепідключитися. Для single-node cluster потрібне окреме maintenance window.
Зафіксуйте поточний стан:
kubectl get nodes -o wide
kubectl get pods -A -o wide --field-selector spec.nodeName=worker-01
kubectl get pdb -A
ssh worker-01 \
'systemctl is-active docker; systemctl is-active containerd; kubelet --version'Забороніть нове планування й виконайте drain. Прапорець `--delete-emptydir-data` видалить ephemeral дані Pod, тому перед командою перевірте, що вони не використовуються як єдине сховище важливого стану.
kubectl cordon worker-01
kubectl drain worker-01 \
--ignore-daemonsets \
--delete-emptydir-data \
--timeout=15mНа node встановіть і налаштуйте containerd за попередніми розділами. Потім визначте спосіб конфігурації kubelet. У kubeadm cluster CRI socket може зберігатися в local kubeadm flags або Node annotation залежно від версії та історії оновлень. Не редагуйте параметр навмання: спочатку знайдіть фактичне значення.
sudo grep -R --line-number \
-- '--container-runtime-endpoint\|containerd.sock\|cri-dockerd.sock' \
/var/lib/kubelet /etc/systemd/system/kubelet.service.d 2>/dev/null
sudo systemctl cat kubeletRuntime endpoint має стати `unix:///run/containerd/containerd.sock`. У kubeadm-керованому середовищі оновлюйте його підтримуваним для вашої версії способом та узгодьте kubelet `cgroupDriver: systemd`. Після зміни перезавантажте units:
sudo systemctl daemon-reload
sudo systemctl restart containerd
sudo systemctl restart kubelet
sudo journalctl -u kubelet -n 100 --no-pager
sudo crictl infoДочекайтеся `Ready`, перевірте runtime у node info й тільки потім поверніть scheduling:
kubectl get node worker-01 -o wide
kubectl describe node worker-01 | grep -E 'Container Runtime|Ready'
kubectl uncordon worker-01
kubectl get pods -A -o wide --field-selector spec.nodeName=worker-01Запустіть контрольний Pod із image із вашого registry, перевірте DNS, network і volume mount. Якщо команда використовує приватний registry, спочатку налаштуйте credentials для containerd через Kubernetes imagePullSecret або підтримувану registry-конфігурацію. Для стабільного release flow images повинні приходити через керований CI/CD pipeline, а не збиратися безпосередньо на worker node.
Docker Engine видаляйте лише після кількох успішних workload cycles та перевірки monitoring. Спочатку зупиніть його й спостерігайте, чи немає прихованих залежностей:
sudo systemctl disable --now docker.service docker.socket
systemctl is-active kubelet
systemctl is-active containerd
sudo crictl psНе видаляйте `/var/lib/docker` у той самий день: це незворотна дія й ускладнить rollback. Узгодьте окреме maintenance window та retention для старих даних після завершення міграції всіх nodes.
Діагностика, registry і типові помилки
Якщо node має статус `NotReady`, починайте з kubelet і CRI socket, а не з перевстановлення кластера:
sudo systemctl status containerd kubelet --no-pager
sudo journalctl -u containerd -u kubelet --since=-15m --no-pager
sudo crictl info
sudo crictl pods
sudo crictl ps -aПомилка `unknown service runtime.v1.RuntimeService` зазвичай означає, що CRI plugin вимкнений, config несумісний із версією containerd або daemon не перечитав файл. Перевірте `disabled_plugins`, `containerd config dump`, logs і перезапустіть service після виправлення.
`ImagePullBackOff` не завжди пов’язаний із runtime. Перевірте image name, tag або digest, DNS, CA, firewall і imagePullSecret. Для self-signed registry встановлюйте CA довіреним способом. Не вимикайте TLS verification глобально. Практична схема приватних images розглянута в статті про налаштування Harbor Registry.
Типова помилка — використовувати `ctr images pull` і очікувати, що Kubernetes побачить image у правильному namespace. `ctr` має власну namespace-модель; CRI використовує namespace `k8s.io`. Для діагностики Kubernetes images застосовуйте `crictl images`, а штатне завантаження залишайте kubelet.
Ще одна помилка — залишити різні cgroup drivers у kubelet і containerd. Наслідки можуть проявитися не одразу: нестабільність після навантаження, проблеми з resource accounting або restart. Для cgroup v2 використовуйте systemd driver в обох компонентах.
Не забудьте про observability. Після dockershim змінюються container identifiers і частина metric labels. Старий agent, який читає Docker socket, перестане бачити workloads. Перевірте logs, metrics, security DaemonSet і node troubleshooting ще на canary node.
Висновок
Containerd є природним runtime для Kubernetes, але не повною заміною всього Docker toolchain. Він бере на себе CRI, images, snapshots і запуск OCI containers через runc. Build, Compose, developer UX і publication workflow залишаються окремими задачами, які можна вирішувати Docker, BuildKit, nerdctl або CI-системою.
Безпечний перехід складається з inventory Docker-залежностей, встановлення підтримуваної containerd LTS, увімкнення CRI, узгодження systemd cgroups, налаштування `crictl` і послідовної міграції nodes через cordon та drain. Кожен node потрібно повернути в scheduling лише після перевірки Ready, network, registry, volumes, logs і monitoring.
Не поспішайте видаляти Docker data або стару конфігурацію. Зберігайте rollback window, мігруйте canary node першим і вимірюйте поведінку реальних workloads. Коли всі інструменти працюють через Kubernetes API та CRI, а images надходять із registry за digest, containerd спрощує runtime layer і прибирає зайвий adapter без втрати керованості платформи.