Kubernetes has become the industry standard for container orchestration, powering everything from small startups to the largest cloud-native platforms on the planet. If you are managing containers with Docker or Docker Compose and have reached the point where you need automated scaling, self-healing, rolling updates, and multi-node deployments, Kubernetes is the next step. This guide takes you from zero to a working Kubernetes cluster using kubeadm on Ubuntu Server, covering the architecture, installation, first deployment, service exposure, and day-to-day management commands.
Prerequisites
Before you begin, make sure you have:
- Two or more machines running Ubuntu Server 22.04 or 24.04 (physical or virtual)
- Each machine with at least 2 GB of RAM and 2 CPUs
- Full network connectivity between all machines (private network recommended)
- Unique hostname, MAC address, and product_uuid on every node
- Terminal access with sudo privileges on all nodes
- Swap disabled on all nodes
What Is Kubernetes?
Kubernetes (often abbreviated as k8s) is an open-source container orchestration platform originally designed by Google and now maintained by the Cloud Native Computing Foundation (CNCF). It automates the deployment, scaling, and management of containerized applications.
At its core, Kubernetes solves a fundamental problem: when you have dozens or hundreds of containers running across multiple servers, you need a system to decide where each container runs, restart containers that fail, scale them up or down based on demand, and manage networking between them. Kubernetes does all of this declaratively — you describe the desired state of your application, and Kubernetes continuously works to make the actual state match.
Key concepts:
- Pod — the smallest deployable unit, wrapping one or more containers
- Service — a stable network endpoint that routes traffic to a set of Pods
- Deployment — a declarative way to manage Pods with replicas and rolling updates
- Namespace — a logical partition to isolate resources within a cluster
- Node — a physical or virtual machine in the cluster
Kubernetes Architecture
A Kubernetes cluster consists of two types of nodes:
Control Plane (Master Node)
The control plane manages the overall cluster state and makes scheduling decisions. Its components include:
- kube-apiserver — the front door to the cluster; all communication goes through the API server
- etcd — a distributed key-value store that holds all cluster configuration and state
- kube-scheduler — assigns Pods to nodes based on resource requirements and constraints
- kube-controller-manager — runs controllers that handle routine tasks like maintaining the correct number of Pod replicas
- cloud-controller-manager — integrates with cloud provider APIs (optional, for cloud deployments)
Worker Nodes
Worker nodes run your application containers. Each worker runs:
- kubelet — the agent that communicates with the control plane and manages Pods on the node
- kube-proxy — handles networking rules so Pods can communicate with each other and with external traffic
- Container runtime — the software that actually runs containers (containerd is the standard)
┌─────────────────────────────────────────┐
│ Control Plane Node │
│ ┌───────────┐ ┌──────────────────┐ │
│ │ API Server│ │ Controller Mgr │ │
│ └───────────┘ └──────────────────┘ │
│ ┌───────────┐ ┌──────────────────┐ │
│ │ etcd │ │ Scheduler │ │
│ └───────────┘ └──────────────────┘ │
└─────────────────────────────────────────┘
│ │
┌────┴────┐ ┌───┴─────┐
│ Worker 1│ │ Worker 2│
│ kubelet │ │ kubelet │
│ kube- │ │ kube- │
│ proxy │ │ proxy │
│ runtime │ │ runtime │
└─────────┘ └─────────┘
Preparing All Nodes
The following steps must be performed on every node in the cluster (control plane and workers alike).
Disable Swap
Kubernetes requires swap to be disabled. The kubelet will not start if swap is active:
# Disable swap immediately
sudo swapoff -a
# Remove swap entries from fstab to persist across reboots
sudo sed -i '/ swap / s/^/#/' /etc/fstab
# Verify swap is off
free -h
Load Required Kernel Modules
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
Configure sysctl Parameters
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
# Apply without reboot
sudo sysctl --system
Verify the settings are applied:
sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward
All three values should return 1.
Installing the Container Runtime (containerd)
Kubernetes needs a container runtime that implements the Container Runtime Interface (CRI). containerd is the industry standard and the recommended choice:
# Install containerd
sudo apt update
sudo apt install -y containerd
# Generate the default configuration
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
# Enable SystemdCgroup (required for kubeadm)
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# Restart and enable containerd
sudo systemctl restart containerd
sudo systemctl enable containerd
Verify containerd is running:
sudo systemctl status containerd
The output should show active (running).
Installing kubeadm, kubelet, and kubectl
These three tools are the foundation of a kubeadm-based cluster:
- kubeadm — bootstraps the cluster
- kubelet — runs on every node and manages Pods
- kubectl — the command-line tool for interacting with the cluster
# Install required packages
sudo apt update
sudo apt install -y apt-transport-https ca-certificates curl gpg
# Add the Kubernetes APT repository signing key
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
# Add the Kubernetes APT repository
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
# Install the packages
sudo apt update
sudo apt install -y kubelet kubeadm kubectl
# Prevent automatic upgrades (critical for cluster stability)
sudo apt-mark hold kubelet kubeadm kubectl
Verify the installation:
kubeadm version
kubectl version --client
Initializing the Control Plane
Run the following command only on the control plane node:
sudo kubeadm init --pod-network-cidr=10.244.0.0/16
The --pod-network-cidr flag specifies the IP range for Pods. The value 10.244.0.0/16 is the default for Flannel. If you plan to use Calico, use 192.168.0.0/16 instead.
After initialization completes, you will see output similar to:
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
Then you can join any number of worker nodes by running the following on each as root:
kubeadm join 192.168.1.100:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:abc123...
Save the kubeadm join command — you will need it to add worker nodes.
Configure kubectl for your user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Verify the control plane is running:
kubectl get nodes
The output will show the control plane node with a NotReady status — this is expected until you install a Pod network add-on.
Installing a Pod Network Add-on
Pods need a network plugin (CNI — Container Network Interface) to communicate with each other across nodes. Without it, the cluster is not functional.
Option A: Install Flannel
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
Option B: Install Calico
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml
Wait for all system Pods to become ready:
kubectl get pods -n kube-system
After a minute or two, all Pods should show Running status. Now check the node status again:
kubectl get nodes
The control plane node should now show Ready.
Joining Worker Nodes
On each worker node, run the kubeadm join command you saved from the control plane initialization:
sudo kubeadm join 192.168.1.100:6443 --token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:abc123...
If you lost the join command, you can regenerate it on the control plane:
kubeadm token create --print-join-command
After joining, verify all nodes from the control plane:
kubectl get nodes
Expected output:
NAME STATUS ROLES AGE VERSION
control-plane Ready control-plane 10m v1.31.0
worker-01 Ready <none> 2m v1.31.0
worker-02 Ready <none> 1m v1.31.0
To label your worker nodes for better identification:
kubectl label node worker-01 node-role.kubernetes.io/worker=worker
kubectl label node worker-02 node-role.kubernetes.io/worker=worker
Deploying Your First Application
Now that the cluster is running, let us deploy a simple nginx web server.
Create a Deployment
kubectl create deployment nginx --image=nginx:latest --replicas=3
This tells Kubernetes: “I want 3 instances of nginx running at all times.” Kubernetes will schedule Pods across your worker nodes.
Check the deployment:
kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
nginx 3/3 3 3 30s
View the individual Pods:
kubectl get pods -o wide
The -o wide flag shows which node each Pod is running on.
Using a YAML Manifest
For production workloads, you should define resources in YAML files. Create a file called nginx-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
Apply it:
kubectl apply -f nginx-deployment.yaml
Exposing Services
Pods are ephemeral and get new IP addresses when they restart. Services provide a stable endpoint to reach your application.
ClusterIP (Internal Only)
The default Service type. Accessible only from within the cluster:
kubectl expose deployment nginx-deployment --port=80 --target-port=80 --type=ClusterIP
kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-deployment ClusterIP 10.96.120.45 <none> 80/TCP 10s
NodePort (External Access via Node IP)
Exposes the Service on a static port on each node’s IP:
kubectl expose deployment nginx-deployment --port=80 --target-port=80 --type=NodePort --name=nginx-nodeport
kubectl get services nginx-nodeport
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-nodeport NodePort 10.96.55.10 <none> 80:31234/TCP 5s
You can now access the application at http://<any-node-ip>:31234.
LoadBalancer (Cloud Environments)
In cloud environments (AWS, GCP, Azure), the LoadBalancer type provisions an external load balancer automatically:
kubectl expose deployment nginx-deployment --port=80 --target-port=80 --type=LoadBalancer --name=nginx-lb
For bare-metal clusters, you can use MetalLB to provide LoadBalancer functionality.
Service YAML Manifest
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30080
type: NodePort
Scaling and Rolling Updates
Scaling Replicas
# Scale up to 5 replicas
kubectl scale deployment nginx-deployment --replicas=5
# Scale down to 2 replicas
kubectl scale deployment nginx-deployment --replicas=2
# Verify
kubectl get deployment nginx-deployment
Rolling Updates
Update the container image with zero downtime:
# Update the image
kubectl set image deployment/nginx-deployment nginx=nginx:1.27-alpine
# Watch the rollout progress
kubectl rollout status deployment/nginx-deployment
Kubernetes will gradually replace old Pods with new ones, ensuring the application remains available throughout the update.
Rollback
If something goes wrong:
# View rollout history
kubectl rollout history deployment/nginx-deployment
# Rollback to the previous version
kubectl rollout undo deployment/nginx-deployment
# Rollback to a specific revision
kubectl rollout undo deployment/nginx-deployment --to-revision=2
Inspecting and Debugging Workloads
Viewing Pod Logs
# Logs from a specific Pod
kubectl logs nginx-deployment-abc123
# Follow logs in real time
kubectl logs -f nginx-deployment-abc123
# Logs from all Pods in a Deployment
kubectl logs -l app=nginx
Executing Commands in a Pod
# Open a shell inside a running Pod
kubectl exec -it nginx-deployment-abc123 -- /bin/bash
# Run a single command
kubectl exec nginx-deployment-abc123 -- cat /etc/nginx/nginx.conf
Describing Resources
# Detailed information about a Pod
kubectl describe pod nginx-deployment-abc123
# Detailed information about a node
kubectl describe node worker-01
Troubleshooting
Pods Stuck in Pending State
This typically means the scheduler cannot find a node with sufficient resources:
kubectl describe pod <pod-name>
Look at the Events section at the bottom. Common causes:
- Insufficient CPU or memory on worker nodes
- Node taints preventing scheduling
- Pod affinity rules that cannot be satisfied
Pods in CrashLoopBackOff
The container starts but immediately exits:
# Check the logs
kubectl logs <pod-name>
kubectl logs <pod-name> --previous
# Check events
kubectl describe pod <pod-name>
Common causes:
- Application error or misconfiguration
- Missing environment variables or config maps
- Failing readiness or liveness probes
Nodes Show NotReady
# Check node conditions
kubectl describe node <node-name>
# On the affected node, check kubelet logs
sudo journalctl -u kubelet -f
Common causes:
- CNI plugin not installed or crashing
- kubelet cannot reach the API server
- Disk pressure, memory pressure, or PID pressure
Cannot Reach Services
# Verify Service endpoints
kubectl get endpoints <service-name>
# Test connectivity from within the cluster
kubectl run debug --image=busybox --rm -it -- wget -qO- http://<service-name>
Regenerating Join Tokens
If the join token has expired (tokens are valid for 24 hours by default):
kubeadm token create --print-join-command
kubectl Commands Reference
| Command | Description |
|---|---|
kubectl get nodes | List all nodes in the cluster |
kubectl get pods | List Pods in the current namespace |
kubectl get pods -A | List Pods in all namespaces |
kubectl get services | List all Services |
kubectl get deployments | List all Deployments |
kubectl describe pod <name> | Show detailed Pod information |
kubectl logs <pod> | View Pod logs |
kubectl exec -it <pod> -- /bin/bash | Open shell in a Pod |
kubectl apply -f <file> | Apply a YAML manifest |
kubectl delete -f <file> | Delete resources from a YAML manifest |
kubectl scale deployment <name> --replicas=N | Scale a Deployment |
kubectl rollout status deployment/<name> | Check rollout progress |
kubectl rollout undo deployment/<name> | Rollback a Deployment |
kubectl top nodes | Show node resource usage |
kubectl top pods | Show Pod resource usage |
kubectl config get-contexts | List available cluster contexts |
kubectl cluster-info | Display cluster endpoint information |
Summary
You now have a working Kubernetes cluster deployed with kubeadm, complete with a control plane, worker nodes, a CNI network plugin, and your first application running with exposed services. This is the same foundational architecture used in production Kubernetes environments worldwide.
Key takeaways:
- Kubernetes orchestrates containers across multiple nodes with automated scheduling, scaling, and self-healing
- The control plane (API server, etcd, scheduler, controller manager) manages cluster state while worker nodes run your workloads
- kubeadm is the official tool for bootstrapping production-grade clusters
- Deployments provide declarative management of Pods with rolling updates and rollbacks
- Services (ClusterIP, NodePort, LoadBalancer) give Pods stable network endpoints
- Always use YAML manifests for production workloads to enable version control and reproducibility
For containerization fundamentals that complement this guide, see our tutorials on How to Install Docker on Ubuntu and Docker Compose: Practical Guide for Sysadmins.