Contents

Build a home Kubernetes cluster: part 1, k3s server

This guide installs the control plane for a small home Kubernetes cluster on an x86_64 Ubuntu NAS. Raspberry Pi 5 machines will join it as worker nodes in the next part of the series. The NAS remains the only k3s server and does not run ordinary application workloads.

Target environment: Ubuntu Server 26.04 LTS on x86_64 and k3s v1.36.2+k3s1. Validate the procedure on the NAS before changing this note to Tested on.

This is deliberately a single-control-plane design. It is small and easy to operate, but the Kubernetes API and all cluster state become unavailable when the NAS is offline. Existing containers on worker nodes may continue running, but Kubernetes cannot schedule or reconcile workloads until the NAS returns.

Architecture

The finished platform will keep three different kinds of storage separate:

Storage Location Purpose
k3s state NAS system SSD Kubernetes API state and configuration
Garage NAS storage pool S3-compatible object storage for backups, logs, traces, and artifacts
NFS NAS storage pool Persistent volumes for Kubernetes applications

Garage is not a Kubernetes persistent-volume backend. Applications that need an S3 API use Garage directly over the LAN; applications that need a mounted filesystem request an NFS-backed persistent volume.

The first installation keeps only the k3s components that are needed now:

  • SQLite is the embedded datastore for the single server;
  • Flannel VXLAN provides the pod network;
  • CoreDNS provides cluster DNS;
  • metrics-server provides basic resource metrics;
  • Traefik and ServiceLB remain disabled until ingress is designed;
  • local-path provisioner remains disabled because persistent volumes will come from the NAS over NFS.

The server node is tainted with NoSchedule. System components can still run there when they have the appropriate toleration, but ordinary workloads will be placed on the Raspberry Pi workers.

Prerequisites

The examples assume:

  • the NAS has a static address, represented by NAS_IP;
  • the local network is represented by LAN_CIDR, for example 192.168.50.0/24;
  • k3s.home.arpa resolves to NAS_IP in local DNS;
  • the default pod and service networks, 10.42.0.0/16 and 10.43.0.0/16, do not overlap the LAN, VPN, or Docker networks;
  • the system SSD has persistent space under /var/lib/rancher/k3s;
  • all cluster machines have unique hostnames and synchronized clocks.

Do not place the k3s data directory on NFS. The embedded datastore belongs on local reliable storage, preferably an SSD. The NAS storage pool can still hold backups of that state.

Check the host before installing anything:

1
2
3
4
5
6
hostnamectl
uname -m
ip -brief address
findmnt /var/lib/rancher 2>/dev/null || true
timedatectl status
df -h /

The architecture must be x86_64, the NAS address must be stable, and time synchronization must be active.

Install the small set of tools used by this guide:

1
2
sudo apt update
sudo apt install --yes ca-certificates curl

Prepare the network

With the default Flannel VXLAN backend, every cluster node must be able to reach every other cluster node on UDP port 8472. Worker nodes also need TCP port 6443 on the NAS for the Kubernetes API. These ports must never be exposed to the Internet.

Traffic Protocol and port Source Destination
Kubernetes API TCP 6443 Cluster nodes and administrator LAN NAS
Flannel VXLAN UDP 8472 Every cluster node Every cluster node

If a host firewall is active, create equivalent rules limited to LAN_CIDR. Also allow traffic from the pod and service CIDRs according to the firewall’s forwarding model. Do not blindly enable a new firewall over a remote SSH session: verify that its SSH rule is present first.

Confirm that the chosen address and local DNS record agree:

1
2
getent ahostsv4 k3s.home.arpa
ip route get NAS_IP

After the server is installed, the API port can be checked from another LAN machine with:

1
nc -vz NAS_IP 6443

Create the k3s configuration

k3s loads /etc/rancher/k3s/config.yaml on every start. Keeping durable settings in this file is easier to audit than hiding them in a long installer command.

Create the directory and configuration, replacing NAS_IP with the real static address:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
sudo install -d -m 0755 /etc/rancher/k3s

sudo tee /etc/rancher/k3s/config.yaml >/dev/null <<'EOF'
node-name: k3s-server
node-ip: "NAS_IP"
advertise-address: "NAS_IP"
tls-san:
  - "NAS_IP"
  - "k3s.home.arpa"
secrets-encryption: true
write-kubeconfig-mode: "0600"
flannel-backend: vxlan
node-taint:
  - "node-role.kubernetes.io/control-plane=true:NoSchedule"
disable:
  - traefik
  - servicelb
  - local-storage
EOF

sudo sed -i 's/NAS_IP/192.0.2.10/g' /etc/rancher/k3s/config.yaml
sudo sed -n '1,200p' /etc/rancher/k3s/config.yaml

Replace 192.0.2.10 in the sed command with the NAS address. The example address belongs to the documentation-only TEST-NET-1 range and will not work on a LAN.

tls-san allows administrators and workers to use either the stable address or the local DNS name without a certificate-name error. Secret encryption protects Kubernetes Secret values in the datastore if its files are copied or backed up without authorization.

Install a pinned k3s release

Pinning the release makes the initial deployment reproducible. Upgrades should be a separate, deliberate operation after the current release notes and backup have been checked.

Download the official installer and inspect its source before running it:

1
2
curl -fsSL https://get.k3s.io -o /tmp/install-k3s.sh
less /tmp/install-k3s.sh

Install k3s v1.36.2+k3s1 as a systemd service:

1
2
sudo env INSTALL_K3S_VERSION='v1.36.2+k3s1' \
  sh /tmp/install-k3s.sh

The installer downloads the matching binary and checksum, creates the k3s service, enables it at boot, and starts it. It also installs kubectl, crictl, and ctr command links when they do not conflict with existing commands.

Verify the control plane

Wait for the API and the packaged components to become ready:

1
2
3
4
5
sudo systemctl status k3s --no-pager
sudo k3s kubectl wait --for=condition=Ready node/k3s-server --timeout=120s
sudo k3s kubectl get nodes -o wide
sudo k3s kubectl get pods -A -o wide
sudo k3s kubectl get storageclass

Expected results:

  • k3s-server is Ready and has the control-plane and master roles;
  • CoreDNS and metrics-server become Running;
  • there are no Traefik or ServiceLB pods;
  • no default StorageClass exists yet.

Confirm the pinned versions and node taint:

1
2
3
sudo k3s --version
sudo k3s kubectl version
sudo k3s kubectl describe node k3s-server | sed -n '/Taints:/,/Unschedulable:/p'

The taint must include:

1
node-role.kubernetes.io/control-plane=true:NoSchedule

Check the service log if a component does not become ready:

1
sudo journalctl -u k3s --since '-15 minutes' --no-pager

Configure kubectl on an administrator machine

The server kubeconfig contains both administrator credentials and an endpoint that initially points to localhost. Treat it as a secret.

On the NAS, make a temporary user-readable copy:

1
2
3
sudo install -m 0600 -o "$USER" -g "$(id -gn)" \
  /etc/rancher/k3s/k3s.yaml "$HOME/k3s.yaml"
sed -i 's#https://127.0.0.1:6443#https://k3s.home.arpa:6443#' "$HOME/k3s.yaml"

Transfer it over an authenticated channel to ~/.kube/home-k3s.yaml on the administrator machine, then delete the temporary copy from the NAS:

1
rm -f "$HOME/k3s.yaml"

On the administrator machine:

1
2
3
chmod 0600 ~/.kube/home-k3s.yaml
export KUBECONFIG="$HOME/.kube/home-k3s.yaml"
kubectl get nodes -o wide

Anyone who has this file has full administrator access to the cluster. Never commit it to a repository or store it in an unencrypted note.

Protect the cluster state

The default datastore for a single k3s server is SQLite. Its database, server token, certificates, and encryption configuration live below /var/lib/rancher/k3s/server. A file-level copy of a live SQLite database is not a reliable backup, so stop k3s briefly before making the initial archive:

1
2
3
4
5
sudo systemctl stop k3s
sudo tar --xattrs --acls -C /var/lib/rancher/k3s \
  -czf /root/k3s-server-initial-backup.tar.gz server
sudo systemctl start k3s
sudo k3s kubectl get nodes

Store the archive outside the NAS and protect it like the kubeconfig: it contains credentials that control the cluster. A later part of the series will add recurring workload backups with Velero, but Velero does not replace a backup of the k3s server state.

Retrieve the worker join token

The next guide will use the agent token to attach Raspberry Pi workers. Display it only when needed:

1
sudo cat /var/lib/rancher/k3s/server/node-token

Do not publish or commit this value. A machine with network access to the API and a valid token can join the cluster.

At this point the NAS provides a minimal, persistent k3s control plane. The next step is to prepare Raspberry Pi 5 nodes, install the same k3s release as agents, and verify scheduling and pod networking across architectures.

Recovery and removal

Restart k3s after changing /etc/rancher/k3s/config.yaml:

1
2
sudo systemctl restart k3s
sudo journalctl -u k3s -n 100 --no-pager

The installer creates an uninstall script. Running it deletes the local k3s service, containers, datastore, and cluster configuration from this server. Use it only when intentionally rebuilding the control plane:

1
sudo /usr/local/bin/k3s-uninstall.sh

Uninstalling the only server destroys the Kubernetes cluster state. Preserve a verified backup before using this command.

References