Most Kubernetes tutorials start with a managed cluster in the cloud. Here it's different: we'll set up the cluster on VMs, in your network, so you can see the plumbing. It hurts a little — and that's exactly why it teaches.
This course's lab has 3 masters and 3 workers. In chapter 05 we'll make the control plane HA; for now, one master is enough to learn.
Course seal: tested on Ubuntu Server 24.04, Kubernetes 1.35 and containerd 2.x. IPs below are fictional (
192.168.56.x).
Topology
| Name | IP | vCPU | RAM | Disk | Role |
|---|---|---|---|---|---|
| k8s-master-01 | 192.168.56.10 | 2 | 4 GB | 50 GB | control plane |
| k8s-master-02 | 192.168.56.11 | 2 | 4 GB | 50 GB | control plane (HA, chap. 05) |
| k8s-master-03 | 192.168.56.12 | 2 | 4 GB | 50 GB | control plane (HA, chap. 05) |
| k8s-worker-01 | 192.168.56.21 | 4 | 16 GB | 100 GB | worker |
| k8s-worker-02 | 192.168.56.22 | 4 | 16 GB | 100 GB | worker |
| k8s-worker-03 | 192.168.56.23 | 4 | 16 GB | 100 GB | worker |
Why 16 GB on workers? Because we'll run database, messaging, object storage, and observability. In a lab, 8 GB works until you start testing load and failover — and then the OOM killer shows up. I learned this the hard way.
If you don't have 6 VMs, start with 1 master + 1 worker. The important thing is the process.
1. Preparing the VMs
Create the VMs in Proxmox (or VirtualBox/UTM) with Ubuntu Server 24.04. During installation:
- User with sudo.
- OpenSSH enabled.
- No swap (we'll disable it anyway).
Then, on all VMs:
sudo hostnamectl set-hostname k8s-master-01 # ajuste por VM
sudo timedatectl set-timezone America/Sao_Paulo
sudo apt update && sudo apt upgrade -y
Configure /etc/hosts on all (or use internal DNS):
192.168.56.10 k8s-master-01
192.168.56.11 k8s-master-02
192.168.56.12 k8s-master-03
192.168.56.21 k8s-worker-01
192.168.56.22 k8s-worker-02
192.168.56.23 k8s-worker-03
Test resolution and clock. Wrong clock breaks TLS and join:
getent hosts k8s-master-01
timedatectl
2. Node prerequisites
On all VMs:
# swap off (agora e no boot)
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
# módulos de kernel
sudo tee /etc/modules-load.d/k8s.conf >/dev/null <<'EOF'
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
# sysctl de rede
sudo tee /etc/sysctl.d/k8s.conf >/dev/null <<'EOF'
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
Verify:
free -h # swap deve ser 0
lsmod | grep -E 'overlay|br_netfilter'
sysctl net.ipv4.ip_forward
Don't skip swap. Kubelet refuses to start with active swap by default. It's the #1 mistake of those setting up their first cluster.
3. containerd
Kubernetes doesn't talk to Docker directly; it talks to a runtime via CRI. We'll use containerd.
sudo apt install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
Edit /etc/containerd/config.toml and ensure:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
Restart:
sudo systemctl restart containerd
sudo systemctl enable containerd
systemctl status containerd --no-pager
4. kubeadm, kubelet and kubectl
sudo apt install -y apt-transport-https ca-certificates curl gpg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.35/deb/Release.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.35/deb/ /' \
| sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt update
sudo apt install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
Check:
kubeadm version
kubelet --version
kubectl version --client
5. kubeadm init
On master-01:
sudo kubeadm init \
--pod-network-cidr=10.244.0.0/16 \
--apiserver-advertise-address=192.168.56.10 \
--control-plane-endpoint=192.168.56.10
At the end, it prints the join command. Save it. Configure your user's kubeconfig:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
kubectl get nodes
The master will appear NotReady until we install the CNI. It's normal.
6. CNI (flannel)
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
kubectl -n kube-flannel get pods -o wide
kubectl get nodes
When flannel is Running on all nodes, the master becomes Ready.
Warning: flannel solves networking, but does not enforce NetworkPolicy. If you need segmentation, chapter 06 explains Cilium/Calico. I found this out the hard way: the policy existed, but had no effect.
7. Joining workers
On the master, generate a new token if needed:
kubeadm token create --print-join-command
Run the command on the workers (as root):
sudo kubeadm join 192.168.56.10:6443 --token <token> \
--discovery-token-ca-cert-hash sha256:<hash>
Validate:
kubectl get nodes -o wide
kubectl get pods -A
All nodes should be Ready.
8. The first 'didn't come up'
Some errors you will encounter:
| Symptom | Likely cause | How to resolve |
|---|---|---|
| kubelet doesn't start | active swap | swapoff -a + /etc/fstab |
Node NotReady |
missing CNI or flannel pod with error | kubectl -n kube-flannel logs <pod> |
join fails due to TLS |
unsynchronized clock | timedatectl, NTP |
connection refused on 6443 |
firewall or apiserver down | ss -lntp, crictl ps |
flannel without subnet.env after reboot |
/run cleaned and flannel started before |
restart the DaemonSet/wait; check flannel logs |
pods in Pending |
node with taint or few resources | kubectl describe node |
9. Cleanup (when you need to redo)
sudo kubeadm reset -f
sudo rm -rf /etc/cni/net.d /var/lib/cni /var/lib/kubelet
sudo systemctl restart containerd kubelet
Do this on all nodes and start over. That's what labs are for.
Checklist
- [ ] 6 VMs with hostname, fixed IP and correct
/etc/hosts. - [ ] Swap disabled and modules loaded on all nodes.
- [ ] containerd with
SystemdCgroup = true. - [ ] kubeadm/kubelet/kubectl on the same version.
- [ ]
kubeadm initcompleted and kubeconfig copied. - [ ] CNI applied and master
Ready. - [ ] Workers with
joincompleted andReady. - [ ]
kubectl get pods -Awithout broken pods.
Glossary
- kubeadm: cluster bootstrap tool.
- CRI: interface between kubelet and runtime.
- CNI: pod network plugin.
- taint/toleration: affinity/repulsion rule between pods and nodes.
- join: command that adds a node to the cluster.
Next
With the cluster up and running, we'll create the first Deployment, expose a Service, and learn to diagnose when the pod doesn't start.
Next: Kubernetes Course 03 — The first pod and the first deployment
Keep reading
Read also

StorCLI - Complete Cheat Sheet
StorCLI is the official and modern tool for managing Broadcom/LSI RAID controllers. This guide covers essential commands, syntax and use cases.
12 min de leitura
TutoriaisBusiness Services in Nyverra Portal: Translating IT into Business
The business doesn't feel a server go down — it feels email stop. The Business Services (Service Mapping) module of Nyverra ITSM Portal connects services like Email, ERP or PACS to CMDB assets, revealing the real impact of each failure.
5 min de leitura
SegurançaRPO vs RTO: How to Define Before Buying Backup (and Why Most Companies Get It Wrong)
Most companies buy backup asking the wrong question: "how much does it cost?". The right question is: how much data can I lose (RPO) and how long can I be down (RTO). Discover how to define these numbers per system and avoid purchasing protection based on price, not on real need.
9 min de leitura
Comments
No comments yet
Be the first to share your thoughts on this article.

Leave a comment