K3s Cluster on Three Mac mini Late 2014 Nodes

2025-05-16SystemsKubernetesHomelab

  • k3s
  • kubernetes
  • ubuntu
  • mac-mini
  • homelab
  • devops

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:

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:

The worker nodes run:


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:


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:

  1. Upgrade the server node.
  2. Upgrade one worker at a time.
  3. 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:

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:

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:

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:


22. Next Improvements

Possible next steps:


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.