# 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](https://github.com/k3s-io/k3s/releases/tag/v1.36.2%2Bk3s1).
> 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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
sudo k3s --version
sudo k3s kubectl version
sudo k3s kubectl describe node k3s-server | sed -n '/Taints:/,/Unschedulable:/p'
```

The taint must include:

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

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

```bash
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:

```bash
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:

```bash
rm -f "$HOME/k3s.yaml"
```

On the administrator machine:

```bash
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:

```bash
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:

```bash
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`:

```bash
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:

```bash
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

- [k3s quick-start guide](https://docs.k3s.io/quick-start)
- [k3s installation requirements](https://docs.k3s.io/installation/requirements)
- [k3s configuration file](https://docs.k3s.io/installation/configuration#configuration-file)
- [k3s packaged components](https://docs.k3s.io/installation/packaged-components)
- [k3s server options](https://docs.k3s.io/cli/server)

