Skip to content
Field Notes
Go back

Home Server Part 3: Cluster Setup — Ubuntu Server, K3s, and Traefik

Part 3 of the Home Server Series. Follows Part 2: Requirements and Decisions.

The foundation: installing the OS, Kubernetes, storage, and ingress routing. After this post, you have a working cluster ready to deploy services.


TL;DR

Ubuntu Server 24.04 LTS with LVM (leave 20-30% unallocated for growth). K3s installed with --data-dir /data/k3s. Traefik (bundled) configured for host-based routing via IngressRoute CRDs. DNS via /etc/hosts on client machines.


1. Ubuntu Server Installation

1.1 Create Bootable USB

Download Ubuntu Server 24.04 LTS from ubuntu.com. Flash to USB:

# On macOS
diskutil list                          # find your USB (e.g., /dev/disk4)
diskutil unmountDisk /dev/disk4
sudo dd if=ubuntu-24.04-live-server-amd64.iso of=/dev/rdisk4 bs=4M

Or use balenaEtcher for a GUI.

1.2 Install

Boot from USB. During installation:

  1. Language/keyboard: your preference
  2. Network: configure a static IP (or set a DHCP reservation on your router later — you want a stable IP)
  3. Storage layout: choose “Use an entire disk” → enable LVM group
  4. LVM customisation (edit before confirming):
Logical VolumeMountSizePurpose
ubuntu-lv (root)/50 GBOS, packages, K3s binaries
data-lv/data60-70% of remainingK3s storage, app data, media
swapswap4-8 GBSwap space
unallocated20-30% of VGFuture growth

Why leave space unallocated? LVM lets you grow volumes live: sudo lvextend -L +100G /dev/ubuntu-vg/data-lv && sudo resize2fs /dev/ubuntu-vg/data-lv. You can’t easily shrink, so start conservative.

  1. SSH: enable OpenSSH server
  2. User: create your admin user
  3. Snaps: skip all optional snaps

1.3 Post-Install

SSH into your server:

ssh youruser@SERVER_IP
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget git htop net-tools

Create the data directory structure:

sudo mkdir -p /data/{k3s-storage,media,photos,databases,registry}
sudo chown -R $USER:$USER /data

1.4 Two-Disk Setup (if applicable)

If you have two SSDs, use the second one entirely for /data:

lsblk                                    # identify the second disk

sudo pvcreate /dev/sdb
sudo vgcreate data-vg /dev/sdb
sudo lvcreate -l 80%FREE -n data-lv data-vg
sudo mkfs.ext4 /dev/data-vg/data-lv

sudo mkdir -p /data
echo '/dev/data-vg/data-lv /data ext4 defaults 0 2' | sudo tee -a /etc/fstab
sudo mount -a

Leave 20% of the VG free for snapshots and future growth.


2. K3s Installation

2.1 Install

curl -sfL https://get.k3s.io | sh -s - \
  --data-dir /data/k3s \
  --write-kubeconfig-mode 644

Verify:

kubectl get nodes
# Should show your node as Ready

kubectl get pods -A
# Should show traefik, coredns, local-path-provisioner, etc.

2.2 kubectl Setup

echo 'export KUBECONFIG=/etc/rancher/k3s/k3s.yaml' >> ~/.bashrc
source ~/.bashrc
kubectl cluster-info

2.3 Configure Storage

Redirect the local-path-provisioner to your data volume:

kubectl edit configmap local-path-config -n kube-system

Change config.json to:

{
  "nodePathMap": [{
    "node": "DEFAULT_PATH_FOR_NON_LISTED_NODES",
    "paths": ["/data/k3s-storage"]
  }]
}

2.4 Custom StorageClasses

For directing workloads to specific paths:

# storage-classes.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-media
provisioner: rancher.io/local-path
parameters:
  nodePath: /data/media
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-databases
provisioner: rancher.io/local-path
parameters:
  nodePath: /data/databases
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-photos
provisioner: rancher.io/local-path
parameters:
  nodePath: /data/photos
kubectl apply -f storage-classes.yaml

2.5 Resource Overhead

ComponentRAMCPU
K3s control plane~500-750 MB~5-6% of one core
Traefik~50-100 MBminimal
CoreDNS~20-30 MBminimal
local-path-provisioner~10-20 MBminimal
Total baseline~600-900 MB~6-8%

3. Traefik Ingress Configuration

K3s ships Traefik v3 as the default ingress controller. It’s already running.

3.1 Customise Traefik

# traefik-config.yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    api:
      dashboard: true
      insecure: false
    logs:
      general:
        level: INFO
    ports:
      websecure:
        tls:
          enabled: true
sudo cp traefik-config.yaml /var/lib/rancher/k3s/server/manifests/
# K3s auto-applies manifests placed here

3.2 Local DNS Setup

For host-based routing (jellyfin.home, grafana.home), add entries on your client machines:

# /etc/hosts on your laptop/desktop
SERVER_IP   jellyfin.home grafana.home immich.home traefik.home torrent.home photos.home myapp.home

Or configure your router’s DNS for wildcard resolution if supported.

3.3 Traefik Dashboard

# traefik-dashboard.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: dashboard-auth
  namespace: kube-system
spec:
  basicAuth:
    secret: dashboard-auth-secret
---
apiVersion: v1
kind: Secret
metadata:
  name: dashboard-auth-secret
  namespace: kube-system
type: kubernetes.io/basic-auth
stringData:
  username: admin
  password: CHANGE_THIS_PASSWORD
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: traefik-dashboard
  namespace: kube-system
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`traefik.home`)
      kind: Rule
      middlewares:
        - name: dashboard-auth
      services:
        - name: api@internal
          kind: TraefikService
  tls: {}
kubectl apply -f traefik-dashboard.yaml

Visit https://traefik.home to see all routes and services.


4. Verify Everything Works

# Node status
kubectl get nodes

# All system pods running
kubectl get pods -A

# Storage classes available
kubectl get storageclass

# Traefik listening
kubectl get svc -n kube-system traefik

# Data directory structure
ls -la /data/

The cluster is ready. Next posts deploy services into it:


References


Share this post on:

Previous Post
Home Server Part 2: Requirements and Decisions — Why We Chose This Stack
Next Post
Home Server Part 5: Monitoring — Grafana and Prometheus