1
2
3
4
5
6
7
Author: Xiaohui Li

WeChat: lxh_chat

Email: 939958092@qq.com

TelegramGroup: https://t.me/itskillshare

Still Running K8s with Docker? Time to Upgrade!

This tutorial shows you how to deploy the latest Kubernetes 1.33.1 cluster on Rocky Linux 9.4, using the more native container runtime containerd 2.1.1 together with the powerful CLI tool nerdctl 2.1.2.

We’ll skip Docker entirely and go for a pure containerd adventure. The guide starts from zero, covering environment preparation, containerd configuration, nerdctl testing, Kubernetes installation and initialization, and finally cluster reset. Step by step, you’ll end up with a production‑ready, high‑quality cluster.

If you’re curious about nerdctl’s usability, or want to deploy Kubernetes without Docker, this article is for you.


Project Details

  • K8s version: 1.33.1
  • OS: Rocky Linux 9.4
  • Master hostname: k8s-master
  • Worker hostname: k8s-worker1
  • containerd version: v2.1.1

Prepare DNS Resolution

Consistent names make kubeadm output, logs, and joins predictable. It also avoids reverse DNS surprises.

On all nodes, edit /etc/hosts:

1
2
3
4
cat >> /etc/hosts <<EOF
192.168.8.200 k8s-master
192.168.8.201 k8s-worker1
EOF

Install nerdctl

Download and extract:

1
2
wget https://github.com/containerd/nerdctl/releases/download/v2.1.2/nerdctl-full-2.1.2-linux-amd64.tar.gz
tar Cxzvvf /usr/local nerdctl-full-2.1.2-linux-amd64.tar.gz

Check containerd Version

1
2
containerd -v
# containerd github.com/containerd/containerd/v2 v2.1.1 ...

Configure containerd

The default config lacks registry mirrors and uses upstream pause images that may be slow or blocked.

1
2
mkdir /etc/containerd
containerd config default > /etc/containerd/config.toml

Replace pause image with a domestic mirror:

if you can access docker hub or k8s registry, you do not need do that.

1
sed -i "s|sandbox = 'registry.k8s.io/pause:3.10'|sandbox = 'registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.10'|" /etc/containerd/config.toml

Set registry config path:

if you can access docker hub or k8s registry, you do not need do that.

1
sed -i '/\[plugins."io.containerd.cri.v1.images".registry\]/{n;s|.*|  config_path = "/etc/containerd/certs.d"|}' /etc/containerd/config.toml

Create registry mirror config:

1
2
3
4
5
6
mkdir -p /etc/containerd/certs.d/docker.io
cat > /etc/containerd/certs.d/docker.io/hosts.toml <<'EOF'
server = "https://registry.myk8s.cn"
[host."https://registry.myk8s.cn"]
capabilities = ["pull", "resolve", "push"]
EOF

Start Services

1
2
3
systemctl daemon-reload
systemctl enable --now containerd
systemctl enable --now buildkit

Enable nerdctl Autocompletion

1
2
nerdctl completion bash > /etc/bash_completion.d/nerdctl
source /etc/bash_completion.d/nerdctl

Create First Container

1
2
nerdctl run -d -p 8000:80 --name container1 registry.myk8s.cn/library/nginx
nerdctl ps

Enter container:

1
2
3
nerdctl exec -it container1 /bin/bash
echo hello lixiaohui > /usr/share/nginx/html/index.html
exit

Test:

1
2
curl http://127.0.0.1:8000
# hello lixiaohui

Deploy Kubernetes

Disable Swap

Kubelet refuses to start when swap is enabled to ensure predictable memory accounting.

1
2
swapoff -a
sed -i 's/.*swap.*/#&/' /etc/fstab

Enable iptables Bridge

Kubernetes networking relies on bridged traffic being visible to iptables and routing packets between pods and services.

1
2
3
4
5
6
7
8
9
10
11
12
13
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
br_netfilter
EOF

modprobe br_netfilter

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF

sysctl --system

Install kubeadm, kubelet, kubectl

Faster, consistent RPM delivery aligned with the Kubernetes version you target.

1
2
3
4
5
6
7
8
9
10
11
12
cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.33/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.33/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF

yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
systemctl enable --now kubelet

Enable completion:

1
2
kubectl completion bash > /etc/bash_completion.d/kubectl
kubeadm completion bash > /etc/bash_completion.d/kubeadm

Configure crictl

crictl is the primary tool to inspect pods/images directly via CRI when debugging kubelet/runtime issues.

1
2
3
4
5
6
cat > /etc/crictl.yaml <<'EOF'
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 10
debug: false
EOF

Test:

1
crictl images

Open Firewall Ports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
sudo firewall-cmd --zone=public --add-protocol=ipip --permanent

sudo firewall-cmd --zone=public --add-port=179/tcp --permanent

sudo firewall-cmd --zone=public --add-port=4789/udp --permanent

sudo firewall-cmd --zone=public --add-port=51820/udp --permanent

sudo firewall-cmd --zone=public --add-port=51821/udp --permanent

sudo firewall-cmd --zone=public --add-port=5473/tcp --permanent

sudo firewall-cmd --zone=public --add-port=443/tcp --permanent

sudo firewall-cmd --zone=public --add-port=6443/tcp --permanent

sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent

sudo firewall-cmd --zone=public --add-port=5443/tcp --permanent

sudo firewall-cmd --zone=public --add-port=9090/tcp --permanent

sudo firewall-cmd --zone=public --add-port=9081/tcp --permanent

sudo firewall-cmd --zone=public --add-port=9900/tcp --permanent

sudo firewall-cmd --zone=public --add-port=9200/tcp --permanent

sudo firewall-cmd --zone=public --add-port=9443/tcp --permanent

sudo firewall-cmd --zone=public --add-port=5444/tcp --permanent

sudo firewall-cmd --zone=public --add-port=5601/tcp --permanent

sudo firewall-cmd --zone=public --add-port=8444/tcp --permanent

sudo firewall-cmd --zone=public --add-port=9443/tcp --permanent

sudo firewall-cmd --zone=public --add-port=9449/tcp --permanent

sudo firewall-cmd --zone=public --add-port=4790/udp --permanent

sudo firewall-cmd --reload


Initialize Cluster

A config file centralizes cluster parameters and makes the deployment reproducible.

1
2
3
4
5
6
kubeadm config print init-defaults > kubeadm.yaml
sed -i 's/.*advert.*/ advertiseAddress: 192.168.8.200/g' kubeadm.yaml
sed -i 's/.*name.*/ name: k8s-master/g' kubeadm.yaml
# imagerepo doest not need when you can access k8s repo
sed -i 's|imageRepo.*|imageRepository: registry.cn-hangzhou.aliyuncs.com/google_containers|g' kubeadm.yaml
sed -i "/^\\s*networking:/a\\ podSubnet: 172.16.0.0/16" kubeadm.yaml

What these fields do:

  • advertiseAddress: The API server’s reachable IP for nodes.

  • name: Cluster name for context identification.

  • imageRepository: Speeds component image pulls using a regional mirror.

  • podSubnet: Must match your CNI configuration (Calico CIDR below).

Initialize:

1
kubeadm init --config kubeadm.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.8.200:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:c23e7b2f6db14c9d8d5e7adaed6ec60e08778ef3f421e38cd6c22615d95d50c2


Deploy Calico Network

1
2
3
4
5
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/refs/tags/v3.30.0/manifests/tigera-operator.yaml
wget https://raw.githubusercontent.com/projectcalico/calico/refs/tags/v3.30.0/manifests/custom-resources.yaml
vim custom-resources.yaml
# change cidr to 172.16.0.0/16
kubectl apply -f custom-resources.yaml

Check pods:

1
kubectl get pod -A

Join Worker Nodes

On worker:

1
2
3
kubeadm join 192.168.8.200:6443 --token <token> \
--discovery-token-ca-cert-hash sha256:<hash> \
--cri-socket=unix:///run/containerd/containerd.sock

Label:

1
2
kubectl label nodes k8s-worker1 node-role.kubernetes.io/worker=
kubectl get nodes

Reset Cluster (Optional)

1
2
3
4
kubeadm reset --cri-socket=unix:///run/containerd/containerd.sock
rm -rf /etc/cni/net.d
iptables -F
rm -rf $HOME/.kube/config

Conclusion

We have successfully deployed a Kubernetes cluster on Rocky Linux using containerd and nerdctl, integrated Calico networking, joined worker nodes, and learned how to reset the cluster. This provides a stable environment for containerized applications.