Back to Blog
Kubernetes
K3s
Tutorial
Setting Up a Kubernetes Cluster in Your Homelab
January 10, 2024
12 min read
# Setting Up a Kubernetes Cluster in Your Homelab
Running Kubernetes in your homelab is an excellent way to learn container orchestration, test applications, and build production-ready skills. In this guide, we'll deploy a K3s cluster on Proxmox with multiple nodes and high availability.
## Why K3s?
K3s is a lightweight Kubernetes distribution perfect for homelabs:
- **Lightweight**: Uses only 512MB of RAM
- **Easy Installation**: Single binary, no complex dependencies
- **Full Kubernetes**: 100% compliant with upstream Kubernetes
- **Built-in Features**: Includes Traefik, CoreDNS, and local storage
## Architecture Overview
We'll create a 3-node cluster:
- **1 Control Plane Node**: Manages the cluster
- **2 Worker Nodes**: Run application workloads
```
┌─────────────────┐
│ Control Plane │
│ 192.168.1.10 │
└────────┬────────┘
│
┌────┴────┐
│ │
┌───▼───┐ ┌──▼────┐
│Worker1│ │Worker2│
│ .11 │ │ .12 │
└───────┘ └───────┘
```
## Prerequisites
- Proxmox VE installed and configured
- 3 VMs or LXC containers (Ubuntu 22.04 recommended)
- At least 2GB RAM per node (4GB recommended)
- 2 CPU cores per node
- 20GB storage per node
## Preparing the Nodes
### Create VMs in Proxmox
For each node, create a VM with:
```bash
# CPU: 2 cores
# RAM: 4GB
# Disk: 20GB
# Network: Bridge to vmbr0
```
### Install Ubuntu Server
1. Boot from Ubuntu Server ISO
2. Complete the installation wizard
3. Install OpenSSH server
4. Update the system:
```bash
sudo apt update && sudo apt upgrade -y
```
### Configure Static IP Addresses
Edit `/etc/netplan/00-installer-config.yaml`:
```yaml
network:
version: 2
ethernets:
ens18:
addresses:
- 192.168.1.10/24 # Change for each node
gateway4: 192.168.1.1
nameservers:
addresses: [192.168.1.1, 8.8.8.8]
```
Apply configuration:
```bash
sudo netplan apply
```
### Disable Swap
Kubernetes requires swap to be disabled:
```bash
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
```
### Configure Hostnames
Set unique hostnames for each node:
```bash
# Control plane
sudo hostnamectl set-hostname k3s-master
# Worker nodes
sudo hostnamectl set-hostname k3s-worker1
sudo hostnamectl set-hostname k3s-worker2
```
Update `/etc/hosts` on all nodes:
```
192.168.1.10 k3s-master
192.168.1.11 k3s-worker1
192.168.1.12 k3s-worker2
```
## Installing K3s
### Install Control Plane
On the master node, run:
```bash
curl -sfL https://get.k3s.io | sh -s - server \
--write-kubeconfig-mode 644 \
--disable traefik \
--node-name k3s-master
```
**Note**: We disable Traefik to install it manually later with custom configuration.
Verify installation:
```bash
sudo kubectl get nodes
```
Get the node token for workers:
```bash
sudo cat /var/lib/rancher/k3s/server/node-token
```
Save this token; you'll need it for worker nodes.
### Install Worker Nodes
On each worker node, run:
```bash
curl -sfL https://get.k3s.io | K3S_URL=https://192.168.1.10:6443 \
K3S_TOKEN=YOUR_NODE_TOKEN \
sh -s - agent \
--node-name k3s-worker1 # Change for each node
```
Replace `YOUR_NODE_TOKEN` with the token from the master node.
### Verify Cluster
On the master node:
```bash
kubectl get nodes
```
You should see all three nodes in "Ready" state:
```
NAME STATUS ROLES AGE VERSION
k3s-master Ready control-plane,master 5m v1.27.3+k3s1
k3s-worker1 Ready <none> 2m v1.27.3+k3s1
k3s-worker2 Ready <none> 2m v1.27.3+k3s1
```
## Configure kubectl Access
### Local Machine Access
Copy the kubeconfig from the master node:
```bash
# On master node
sudo cat /etc/rancher/k3s/k3s.yaml
```
On your local machine:
```bash
mkdir -p ~/.kube
# Paste the content and update the server IP
nano ~/.kube/config
```
Change `server: https://127.0.0.1:6443` to `server: https://192.168.1.10:6443`
Test connection:
```bash
kubectl get nodes
```
## Installing Essential Components
### Helm Package Manager
Install Helm on your local machine:
```bash
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
```
### MetalLB Load Balancer
For bare-metal LoadBalancer support:
```bash
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml
```
Create IP pool configuration:
```yaml
# metallb-config.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default
namespace: metallb-system
spec:
addresses:
- 192.168.1.200-192.168.1.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
namespace: metallb-system
spec:
ipAddressPools:
- default
```
Apply configuration:
```bash
kubectl apply -f metallb-config.yaml
```
### Traefik Ingress Controller
Install Traefik with Helm:
```bash
helm repo add traefik https://traefik.github.io/charts
helm repo update
helm install traefik traefik/traefik \
--namespace traefik \
--create-namespace \
--set service.type=LoadBalancer
```
Get Traefik LoadBalancer IP:
```bash
kubectl get svc -n traefik
```
### Cert-Manager
For automatic TLS certificates:
```bash
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.yaml
```
### Longhorn Storage
Distributed block storage for Kubernetes:
```bash
helm repo add longhorn https://charts.longhorn.io
helm repo update
helm install longhorn longhorn/longhorn \
--namespace longhorn-system \
--create-namespace
```
Access Longhorn UI:
```bash
kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80
```
Visit `http://localhost:8080`
## Deploying a Test Application
Create a test deployment:
```yaml
# nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-demo
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- port: 80
targetPort: 80
```
Deploy:
```bash
kubectl apply -f nginx-deployment.yaml
kubectl get svc nginx-service
```
Access the application using the LoadBalancer IP.
## Monitoring with Prometheus
Install kube-prometheus-stack:
```bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install prometheus prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace
```
Access Grafana:
```bash
kubectl port-forward -n monitoring svc/prometheus-grafana 3000:80
```
Default credentials: `admin` / `prom-operator`
## Best Practices
### Resource Limits
Always set resource requests and limits:
```yaml
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
```
### Namespace Organization
Use namespaces to organize workloads:
```bash
kubectl create namespace production
kubectl create namespace development
```
### Backup Strategy
Regular backups with Velero:
```bash
helm repo add vmware-tanzu https://vmware-tanzu.github.io/helm-charts
helm install velero vmware-tanzu/velero \
--namespace velero \
--create-namespace
```
## Troubleshooting
### Pod Not Starting
```bash
kubectl describe pod POD_NAME
kubectl logs POD_NAME
```
### Node Not Ready
```bash
kubectl describe node NODE_NAME
sudo systemctl status k3s
sudo journalctl -u k3s -f
```
### Network Issues
```bash
kubectl get pods -n kube-system
kubectl logs -n kube-system COREDNS_POD
```
## Next Steps
- Implement GitOps with ArgoCD
- Set up CI/CD pipelines
- Configure horizontal pod autoscaling
- Implement network policies
- Deploy service mesh (Istio/Linkerd)
## Conclusion
You now have a fully functional Kubernetes cluster running in your homelab! This setup provides a solid foundation for learning and experimenting with cloud-native technologies.
## Resources
- [K3s Documentation](https://docs.k3s.io/)
- [Kubernetes Official Docs](https://kubernetes.io/docs/)
- [Helm Charts](https://artifacthub.io/)
Keep Exploring
Kubernetes’ Quiet Security Revolution Is Here—and It Might Break Everything Before It Fixes Anything
“Kubernetes’ Quiet Security Revolution Is Here—and It Might Break Everything Before It Fixes
Kubernetes Wants You to Move On From Ingress, Yet Gateway API Feels Like a Puzzle Nobody Asked For
“Kubernetes Wants You to Move On From Ingress, Yet Gateway API Feels Like a Puzzle Nobody Asked
Six Years of Silence Explodes Into One Feature That Could Quietly Rewrite Kubernetes Security Forever
“Six Years of Silence Explodes Into One Feature That Could Quietly Rewrite Kubernetes Security
CVE-2026-22039: How an admission controller vulnerability turned Kubernetes namespaces into a security illusion
Just saw this nasty Kyverno CVE that's a perfect example of why I'm skeptical of admission controllers with god-mode RBAC.