Homelab Part 4: K3s Single Node Kubernetes Setup
Installing K3s on a single Ubuntu VM, configuring kubectl access from Windows, and installing Helm - the foundation everything else in the homelab is built on.
Why Kubernetes for a Home Media Server
Fair question. You could run Jellyfin and Nextcloud with a single docker-compose.yml. Simpler, faster, done in 20 minutes.
But the point of this homelab is to understand how modern infrastructure works - not just to get Jellyfin running. Kubernetes teaches you:
- How container orchestration actually works at the scheduler level
- How networking between services is handled (services, endpoints, DNS)
- How persistent storage is managed separately from compute
- How to deploy and update services without downtime
- Everything that maps directly to AWS EKS, Azure AKS, or GKE in a real job
K3s is the right choice here - it’s full Kubernetes, just without the cloud-provider extras. It runs comfortably on a single VM and uses far less resources than a full kubeadm cluster.
Pre-Installation Checks
SSH into the K3s VM:
1
ssh homelab@192.168.1.51
Verify swap is off (required for Kubernetes):
1
2
free -h
# Swap row must show 0
Verify the hostname is set:
1
2
hostname
# Should return: homelab-k3s
Check available resources:
1
2
3
nproc # CPU cores
free -gh # RAM in GB
df -h / # Disk space
Installing K3s
One command installs the entire K3s cluster:
1
curl -sfL https://get.k3s.io | sh -
What this does:
- Downloads the K3s binary
- Installs it as a systemd service (
k3s) - Starts the Kubernetes control plane (API server, scheduler, controller manager)
- Starts a single worker node (the same machine)
- Writes a kubeconfig to
/etc/rancher/k3s/k3s.yaml
Wait about 30 seconds, then verify:
1
sudo k3s kubectl get nodes
Expected output:
1
2
NAME STATUS ROLES AGE VERSION
homelab-k3s Ready control-plane,master 1m v1.28.x+k3s1
What K3s Installs by Default
K3s comes with some components pre-installed that I’d have to add manually in a full kubeadm cluster:
| Component | What it is |
|---|---|
| CoreDNS | Internal DNS for the cluster (pods resolve each other by name) |
| Traefik | Default ingress controller (I’ll replace this with Nginx) |
| Local Path Provisioner | Default storage class for persistent volumes |
| Klipper LB | Basic service load balancer (I’ll replace with MetalLB) |
I’ll disable Traefik and Klipper during installation because I’m using Nginx Ingress and MetalLB instead. Let me reinstall with those flags:
1
2
3
4
5
6
7
# First, uninstall the default installation
/usr/local/bin/k3s-uninstall.sh
# Reinstall with Traefik and Klipper disabled
curl -sfL https://get.k3s.io | sh -s - \
--disable traefik \
--disable servicelb
Setting Up kubectl on the VM
k3s kubectl works but I prefer the standard kubectl command:
1
2
3
4
5
6
7
8
9
10
11
12
13
# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
# Point kubectl at K3s config
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown homelab:homelab ~/.kube/config
# Test
kubectl get nodes
kubectl get pods -A # all pods across all namespaces
Setting Up kubectl on Windows (Remote Access)
I want to run kubectl commands from my Windows machine without SSHing into the VM every time.
Step 1: Install kubectl on Windows
1
2
3
4
# Using winget
winget install Kubernetes.kubectl
# Or download manually from kubernetes.io/releases
Step 2: Copy the kubeconfig from the VM
1
2
# On the VM: show the kubeconfig content
sudo cat /etc/rancher/k3s/k3s.yaml
Copy this content to your Windows machine at:
1
C:\Users\YourName\.kube\config
Step 3: Fix the server address
The kubeconfig file has server: https://127.0.0.1:6443 - that’s the loopback address, only valid inside the VM. Change it to the VM’s actual IP:
1
server: https://192.168.1.51:6443
Step 4: Test from Windows
1
2
kubectl get nodes
kubectl get pods -A
Installing Helm
Helm is the package manager for Kubernetes. Almost every application I’ll deploy (Nginx Ingress, MetalLB, Longhorn, Jellyfin, Nextcloud) has a Helm chart. Instead of writing 200 lines of YAML from scratch, Helm lets me install with a few commands and a small values file.
1
2
3
4
5
# On the K3s VM
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Verify
helm version
Add the common chart repositories I’ll use:
1
2
3
4
5
6
helm repo add stable https://charts.helm.sh/stable
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add metallb https://metallb.github.io/metallb
helm repo add longhorn https://charts.longhorn.io
helm repo add jetstack https://charts.jetstack.io # cert-manager
helm repo update
Understanding the Cluster Structure
Before moving on, here’s how Kubernetes organises things:
1
2
3
4
5
6
7
8
9
Cluster
└── Namespaces (logical separation)
├── kube-system → core Kubernetes components
├── kube-public → cluster-wide public info
├── default → where apps go if you don't specify
├── networking → I'll put MetalLB + Nginx here
├── storage → Longhorn goes here
├── media → Jellyfin goes here
└── cloud → Nextcloud goes here
I create the namespaces I’ll use:
1
2
3
4
5
kubectl create namespace networking
kubectl create namespace storage
kubectl create namespace media
kubectl create namespace cloud
kubectl get namespaces
Useful kubectl Commands
Quick reference for navigating the cluster:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Nodes
kubectl get nodes -o wide # with IPs and OS info
# Pods
kubectl get pods -A # all namespaces
kubectl get pods -n media # specific namespace
kubectl describe pod <name> -n media # detailed info + events
kubectl logs <pod-name> -n media # container logs
kubectl exec -it <pod-name> -n media -- bash # shell into pod
# Services
kubectl get svc -A # all services with IPs/ports
# Events (useful for debugging)
kubectl get events -n media --sort-by='.lastTimestamp'
# Apply a manifest
kubectl apply -f manifest.yaml
# Delete a resource
kubectl delete -f manifest.yaml
Where I Am Now
At the end of Part 4 I have:
- ✅ K3s running on
192.168.1.51(Traefik and Klipper disabled) - ✅
kubectlworking both on the VM and from Windows - ✅ Helm installed with all needed repos added
- ✅ Namespaces created for networking, storage, media, cloud
- ✅ Basic cluster navigation working
Next: MetalLB and Nginx Ingress - giving the cluster a real IP and routing traffic to the right services.
