K3s Cluster on Three Mac mini Late 2014 Nodes
This document describes my current K3s homelab setup running on three Mac mini Late 2014 machines with Ubuntu.
The goal of this cluster is to have a small, reliable, low-cost Kubernetes environment for learning, self-hosting, experiments, internal tooling, and infrastructure projects.
1. Hardware
The cluster is built using three Apple Mac mini Late 2014 machines.
| Node | Role | Hardware | OS |
|---|---|---|---|
mini-01 |
K3s server / control-plane | Mac mini Late 2014 | Ubuntu |
mini-02 |
K3s agent / worker | Mac mini Late 2014 | Ubuntu |
mini-03 |
K3s agent / worker | Mac mini Late 2014 | Ubuntu |
Recommended baseline per node:
| Component | Recommended |
|---|---|
| CPU | Intel x86_64 |
| RAM | 8 GB or more |
| Storage | SSD preferred |
| Network | Wired Ethernet |
For Kubernetes nodes, wired Ethernet is strongly preferred over Wi-Fi.
2. Network Layout
Example LAN layout:
| Node | Hostname | Example IP | Role |
|---|---|---|---|
| Node 1 | mini-01 |
192.168.0.11 |
Server |
| Node 2 | mini-02 |
192.168.0.12 |
Agent |
| Node 3 | mini-03 |
192.168.0.13 |
Agent |
Recommended router/DHCP setup:
- Reserve a static DHCP lease for each Mac mini.
- Use short, predictable hostnames.
- Keep all nodes on the same LAN/VLAN.
- Avoid changing node IPs after joining the cluster.
Example /etc/hosts entries:
192.168.0.11 mini-01
192.168.0.12 mini-02
192.168.0.13 mini-03
3. Operating System
Each Mac mini runs Ubuntu Server.
Useful base packages:
sudo apt update
sudo apt install -y curl vim htop git net-tools open-iscsi nfs-common
Recommended hostname setup:
sudo hostnamectl set-hostname mini-01
Repeat on each node using its corresponding hostname.
4. K3s Architecture
This cluster uses a simple K3s topology:
mini-01 -> k3s server / control-plane
mini-02 -> k3s agent / worker
mini-03 -> k3s agent / worker
The server node runs:
- Kubernetes API server
- Scheduler
- Controller manager
- Embedded K3s components
- Default SQLite datastore unless configured otherwise
The worker nodes run:
- kubelet
- containerd
- K3s agent
- workload pods
5. Installing K3s Server Node
On mini-01:
curl -sfL https://get.k3s.io | sh -s - server \
--write-kubeconfig-mode 644 \
--node-name mini-01
Check service status:
sudo systemctl status k3s
Check nodes:
kubectl get nodes -o wide
Get the cluster join token:
sudo cat /var/lib/rancher/k3s/server/node-token
Save this token temporarily. It is required to join worker nodes.
6. Joining Worker Nodes
On mini-02 and mini-03:
curl -sfL https://get.k3s.io | K3S_URL=https://192.168.0.11:6443 K3S_TOKEN=<NODE_TOKEN> sh -s - agent \
--node-name mini-02
For mini-03:
curl -sfL https://get.k3s.io | K3S_URL=https://192.168.0.11:6443 K3S_TOKEN=<NODE_TOKEN> sh -s - agent \
--node-name mini-03
Back on the server node:
kubectl get nodes -o wide
Expected result:
NAME STATUS ROLES AGE VERSION
mini-01 Ready control-plane,master ... vX.Y.Z+k3sX
mini-02 Ready <none> ... vX.Y.Z+k3sX
mini-03 Ready <none> ... vX.Y.Z+k3sX
7. Exporting the Kubeconfig
On mini-01, K3s stores the kubeconfig here:
/etc/rancher/k3s/k3s.yaml
Copy it to your local machine:
scp ubuntu@192.168.0.11:/etc/rancher/k3s/k3s.yaml ~/.kube/k3s-mac-mini.yaml
Edit the copied file and replace 127.0.0.1 with the server node IP:
server: https://192.168.0.11:6443
Use it locally:
export KUBECONFIG=~/.kube/k3s-mac-mini.yaml
kubectl get nodes
Optional: merge with your default kubeconfig:
KUBECONFIG=~/.kube/config:~/.kube/k3s-mac-mini.yaml kubectl config view --flatten > /tmp/config
mv /tmp/config ~/.kube/config
chmod 600 ~/.kube/config
8. Cluster Validation
Basic checks:
kubectl get nodes -o wide
kubectl get pods -A
kubectl get svc -A
kubectl get deployments -A
Check K3s service logs:
sudo journalctl -u k3s -f
For worker nodes:
sudo journalctl -u k3s-agent -f
9. Default K3s Components
K3s ships with several useful components enabled by default.
| Component | Purpose |
|---|---|
| CoreDNS | Internal cluster DNS |
| Traefik | Default ingress controller |
| ServiceLB | Lightweight service load balancer |
| Local Path Provisioner | Simple local persistent volumes |
| metrics-server | Resource metrics for pods and nodes |
Check system workloads:
kubectl get pods -n kube-system
10. Storage
By default, K3s includes the local path provisioner.
Example PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Apply it:
kubectl apply -f pvc.yaml
kubectl get pvc
For a more serious homelab, consider:
- Longhorn
- NFS
- OpenEBS
- External NAS-backed storage
11. Ingress
K3s includes Traefik by default.
Example deployment, service, and ingress:
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
spec:
replicas: 2
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: traefik/whoami
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: whoami
spec:
selector:
app: whoami
ports:
- port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami
spec:
rules:
- host: whoami.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80
Apply:
kubectl apply -f whoami.yaml
For local testing, add this to your laptop /etc/hosts:
192.168.0.11 whoami.local
Then open:
http://whoami.local
12. Useful kubectl Commands
kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl describe node mini-01
kubectl top nodes
kubectl top pods -A
kubectl get events -A --sort-by=.metadata.creationTimestamp
Restart a deployment:
kubectl rollout restart deployment/<deployment-name> -n <namespace>
Open a shell inside a pod:
kubectl exec -it <pod-name> -n <namespace> -- sh
View logs:
kubectl logs -f <pod-name> -n <namespace>
13. Node Maintenance
Drain a node before maintenance:
kubectl drain mini-02 --ignore-daemonsets --delete-emptydir-data
Reboot the node:
sudo reboot
Uncordon after it comes back:
kubectl uncordon mini-02
Check status:
kubectl get nodes
14. Upgrading K3s
On the server node:
curl -sfL https://get.k3s.io | sh -
On agent nodes:
curl -sfL https://get.k3s.io | K3S_URL=https://192.168.0.11:6443 K3S_TOKEN=<NODE_TOKEN> sh -
Check version:
kubectl get nodes
kubectl version
Recommended upgrade order:
- Upgrade the server node.
- Upgrade one worker at a time.
- Validate workloads after each node.
15. Backups
Important paths on the server node:
/etc/rancher/k3s/
/var/lib/rancher/k3s/
If using the default SQLite datastore, the database is stored under:
/var/lib/rancher/k3s/server/db/
Simple backup example:
sudo tar -czvf k3s-backup-$(date +%Y%m%d).tar.gz \
/etc/rancher/k3s \
/var/lib/rancher/k3s/server/db
For a production-like setup, use an external datastore or K3s embedded etcd with multiple server nodes.
16. Security Notes
Recommended basics:
- Keep Ubuntu updated.
- Use SSH keys instead of passwords.
- Restrict SSH access to trusted machines.
- Do not expose the Kubernetes API server directly to the internet.
- Store kubeconfig securely.
- Rotate tokens if exposed.
- Use namespaces to separate workloads.
- Use Kubernetes secrets carefully.
Check node token location:
sudo ls -lah /var/lib/rancher/k3s/server/node-token
17. Troubleshooting
Node is not Ready
kubectl describe node <node-name>
sudo journalctl -u k3s -f
sudo journalctl -u k3s-agent -f
DNS issues inside pods
kubectl get pods -n kube-system | grep coredns
kubectl logs -n kube-system -l k8s-app=kube-dns
Worker cannot join cluster
Check:
- Server node IP is reachable.
- Port
6443is reachable. - Token is correct.
- System time is synchronized.
- Hostnames are unique.
Test connectivity:
curl -k https://192.168.0.11:6443
Check open ports
sudo ss -tulpn
18. Full Cluster Export Script
This script collects useful cluster information into a directory that can be archived or committed to a private infra repository.
#!/usr/bin/env bash
set -euo pipefail
OUT="k3s-cluster-export-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$OUT"
kubectl get nodes -o wide > "$OUT/nodes.txt"
kubectl get pods -A -o wide > "$OUT/pods.txt"
kubectl get svc -A -o wide > "$OUT/services.txt"
kubectl get ingress -A -o wide > "$OUT/ingress.txt"
kubectl get deploy -A -o wide > "$OUT/deployments.txt"
kubectl get statefulset -A -o wide > "$OUT/statefulsets.txt"
kubectl get daemonset -A -o wide > "$OUT/daemonsets.txt"
kubectl get pvc -A -o wide > "$OUT/pvcs.txt"
kubectl get pv -o wide > "$OUT/pvs.txt"
kubectl get storageclass -o wide > "$OUT/storageclasses.txt"
kubectl get configmap -A > "$OUT/configmaps.txt"
kubectl get secrets -A > "$OUT/secrets-list-only.txt"
kubectl get events -A --sort-by=.metadata.creationTimestamp > "$OUT/events.txt"
kubectl cluster-info > "$OUT/cluster-info.txt"
kubectl version > "$OUT/version.txt"
# YAML exports. Review before publishing; may contain sensitive data.
kubectl get nodes -o yaml > "$OUT/nodes.yaml"
kubectl get all -A -o yaml > "$OUT/all-resources.yaml"
kubectl get ingress -A -o yaml > "$OUT/ingress.yaml"
kubectl get pvc -A -o yaml > "$OUT/pvcs.yaml"
kubectl get storageclass -o yaml > "$OUT/storageclasses.yaml"
tar -czf "$OUT.tar.gz" "$OUT"
echo "Export written to $OUT.tar.gz"
Do not publish secrets, tokens, kubeconfig files, or private IP details unless intentionally sanitized.
19. Sanitizing Before Publishing
Before publishing the exported configuration to a public blog or GitHub repository, remove:
- Kubeconfig client certificates
- Bearer tokens
- Node tokens
- Secrets
- Internal DNS names if sensitive
- Public IPs if not intended to be disclosed
- Private registry credentials
- Cloud provider credentials
Useful command to inspect possible secrets:
grep -RniE "token|password|secret|key|certificate|auth|bearer" ./k3s-cluster-export-*
20. Topology Diagram
┌──────────────────┐
│ Laptop / kubectl│
└─────────┬────────┘
│
▼
┌──────────────────┐
│ mini-01 │
│ k3s server │
│ 192.168.0.11 │
└───────┬──────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ mini-02 │ │ mini-03 │
│ k3s agent │ │ k3s agent │
│ 192.168.0.12 │ │ 192.168.0.13 │
└──────────────────┘ └──────────────────┘
21. Why This Setup Works Well
This setup is small but useful:
- Three physical machines provide real node scheduling behavior.
- K3s keeps Kubernetes lightweight.
- Mac minis are compact, quiet, and power-efficient.
- Ubuntu keeps the stack familiar and easy to automate.
- The cluster is good enough for learning GitOps, ingress, storage, monitoring, and internal services.
22. Next Improvements
Possible next steps:
- Add Longhorn for replicated storage.
- Add cert-manager for TLS automation.
- Add ExternalDNS or local DNS integration.
- Add Prometheus and Grafana for monitoring.
- Add Argo CD or Flux for GitOps.
- Add a private container registry.
- Add automated backups.
- Add a second K3s server node or migrate to embedded etcd.
Final Notes
Running K3s on old Mac minis is a great way to build a serious homelab without buying expensive server hardware.
It is simple enough to maintain, but realistic enough to practice real Kubernetes operations: node joins, ingress, storage, upgrades, backups, troubleshooting, and workload scheduling.