Rock 5B Single-Node Talos Linux Kubernetes Clusters

In my previous post, Rock 5T Talos Linux Kubernetes Dev Cluster, I detailed how to build a 3-node, dual-NIC, BGP-peered bare-metal powerhouse.

But what about people who don’t actually want/need a High Availability (HA) cluster?

For typical homelab and home automation workloads, HA is often an over-engineered luxury. Plus, with rising hardware prices, buying three high-spec SBCs, multiple NVMe drives and managed PoE switches can quickly turn a fun project into a massive budget drain.

What if you want to run a modern, secure and declarative stack, but on a single board? What if you want to deploy isolated, independent edge nodes across different locations (like a remote site, a family member’s home - greetings to my parents - or a dedicated single-purpose device) without the space, cost or power overhead of a HA cluster?

This guide covers the design and bootstrap of a Single-Node Talos Linux Kubernetes Cluster on the Radxa Rock 5B, driven entirely by GitOps (Flux CD) from a single monorepo.


:thinking: Why run Talos on a single SBC?

(The logical evolution from Armbian + Podman + Ansible)

For years, my standard homelab stack has been a single SBC with Armbian + Podman + Ansible. You write an Ansible playbook, push tasks over SSH and run your Applications … simple.

But if 100% of your applications are already containerized, running a traditional general-purpose OS like Armbian adds extra host-level management layers (like handling local packages, dependencies, users and SSH).

Kubernetes is the state-of-the-art standard because it shifts your focus from managing an operating system to managing workloads, natively handling container lifecycles, routing and self-healing. Pairing Kubernetes with a minimal, API-driven OS like Talos means your host hardware exists purely to run your containers, making the OS virtually maintenance-free.

1. The Fall of the General-Purpose OS

Don’t get me wrong I :heart: Armbian but as a General-Purpose OS it brings a mutable root filesystem, package managers (apt), systemd, cron, a SSH daemon and interactive shells. This opens the door to configuration drift: a manual package upgrade breaks a library, a temporary change is forgotten or a systemd service goes out of sync with your playbook.

  • Talos is completely immutable and ephemeral. It runs with a read-only root filesystem, has no SSH, no shell and no package manager. The entire OS config is a single bulletproof declarative YAML file. It either boots correctly or it doesn’t. No “half-broken” states.

2. Push vs. Pull (Ansible vs. GitOps)

  • Ansible (Push): Executes task-by-task. If a run fails halfway due to an SSH timeout or network blip, your host is left in a dirty, unknown state.
  • Flux/GitOps (Pull): Runs inside your cluster. It continuously monitors your Git repository. If you make a change, it applies it safely. If someone manually alters a resource inside the cluster, Flux instantly detects the drift and reconciles it back to the Git source of truth.

3. Dedicated Network Ingress (L2 ARP vs. Host-Port Clutter)

Under Podman, you map container ports directly to host interfaces (e.g., :8123, :1883) and manage external Nginx reverse proxies with manually templated certificates.

  • With Talos and Cilium, we use the Kubernetes Gateway API and Cilium L2 Announcements.
  • This allows your ingress gateway to get its own dedicated IP address on your home network (e.g., 192.168.1.6) via ARP, completely separate from your node’s management IP (e.g. 192.168.1.5). No port conflicts, no host-port clutter and lightning-fast eBPF network routing.

4. Local-Path vs. Distributed Storage

In a 3-node cluster, running distributed, replicated storage like Longhorn makes perfect sense. On a single-node cluster, however, running distributed storage is a useless performance tax, because it has no other nodes to replicate to and introduces unnecessary CPU / Memory overhead.

For our single-node SBC, we deploy Rancher’s Local Path Provisioner (local-path-provisioner). It is super lightweight and dynamically provisions persistent volumes directly on your high-speed NVMe storage. It gives you raw SSD read/write performance with zero replication overhead.


:brick: Bill of Materials

Component Qty Notes
Radxa Rock 5B (RK3588, 16GB/24GB/32GB) 1 Our target single-node board
Crucial P510 1 TB NVMe SSD 1 Real PCIe 3.0 M.2 performance for etcd and local storage
Radxa 25W PoE+ HAT or 30W+ USB-C PD 1 Power delivery matching the Rock 5B’s footprint and power budget
KKSB Rock 5B Case with Heatsink 1 Beautiful black aluminum enclosure to keep the RK3588 cool and quiet

:world_map: The Plan

There’s one catch with the Radxa Rock 5 series: the BootROM boot order is SPI → eMMC → SD → USB — it cannot boot directly from NVMe. So our plan looks like this:

  1. Pre-flash NVMe with the Talos image on your workstation.
  2. Temporarily boot Armbian from SD to gain access to the SPI NOR flash (/dev/mtdblock0).
  3. Flash U-Boot into SPI, then remove the SD card.
  4. On next boot: SPI U-Boot → NVMe → Talos. :tada:
  5. talosctl apply + bootstrap → Kubernetes is up.
  6. Flux CD + Cilium + Gateway API + Local-Path → it’s a real cluster. :rocket:

Below is the full sequence. Everything should be reproducible.

Reproducible plan

:hammer_and_wrench: Phase 1 — Bake a Talos image for your board

Open factory.talos.dev, pick Talos v1.13.5, “Single Board Computers”, select Radxa Rock 5B as the target board, and add these extensions:

customization:
  systemExtensions:
    officialExtensions:
      - siderolabs/iscsi-tools         # Storage utilities
      - siderolabs/util-linux-tools
      - siderolabs/realtek-firmware    # Required for the onboard Realtek 2.5 GbE NIC
      - siderolabs/lldpd               # SBC shows up in UniFi/smart switches

Download metal-arm64.raw.xz. Note your schematic ID — you will need this in your Talos configuration patch.


:hammer_and_wrench: Phase 2 — Extract U-Boot for SPI

The SPI NOR on the Rock 5B is empty from the factory. We need a U-Boot that knows how to chain-load Talos from NVMe. The binary is bundled inside the sbc-rockchip overlay container:

crane --platform=linux/arm64 export ghcr.io/siderolabs/sbc-rockchip:v0.2.0 \
  | tar x --wildcards '*/rock5b/u-boot-rockchip-spi.bin'

Keep your u-boot-rockchip-spi.bin.


:hammer_and_wrench: Phase 3 — Pre-flash the NVMe

Pop your NVMe SSD into a USB→NVMe adapter on your workstation, and flash the raw Talos image (metal-arm64.raw.xz) using balenaEtcher. Once flashed, install the NVMe drive directly into your Rock 5B.


:hammer_and_wrench: Phase 4 — Flash SPI from Armbian (One-time, per node)

Talos is intentionally minimal — it does not expose /dev/mtd* and cannot flash the SPI NOR from within itself. We use Armbian’s vendor kernel as a one-time stepping stone.

  1. Download Armbian Debian Trixie Minimal (vendor kernel) for the Rock 5B and flash it to a microSD card.
  2. Insert the SD card into the board (the pre-flashed NVMe is already installed — SD wins the boot order). Power on and SSH in as root / 1234.
  3. Verify MTD is available:
    ls /dev/mtd*    # expect /dev/mtdblock0
    
  4. Copy your extracted SPI U-Boot binary over from your workstation:
    scp u-boot-rockchip-spi.bin root@<armbian-ip>:/tmp/
    
  5. Flash and verify:
    dd if=/tmp/u-boot-rockchip-spi.bin of=/dev/mtdblock0 bs=4096
    sync
    md5sum /tmp/u-boot-rockchip-spi.bin
    dd if=/dev/mtdblock0 bs=1 count=$(stat -c%s /tmp/u-boot-rockchip-spi.bin) | md5sum
    # Verify the MD5 checksums match!
    
  6. Note your NIC’s MAC address (ip link show) — you will need it to pin your interface deterministically in Talos.
  7. Power off. Remove the SD card.

From this point forward, the SD card is never needed again. The boot path is permanently: SPI U-Boot → NVMe → Talos.

:hammer_and_wrench: Phase 5 — Talos Config

Machine configuration is split into small patches by concern, making it highly readable, modular and reviewable. Below are the core configuration files from the GitOps repo.

1. Common Patches

talos/common/install.yaml (System Disk & Factory Image)

Specifies the target NVMe storage drive and the custom Talos factory image built with your required kernel extensions and firmware modules.

# Common install config for Radxa Rock 5B SBC nodes (NVMe).
# Build the factory image at https://factory.talos.dev
machine:
  install:
    disk: /dev/nvme0n1
    image: factory.talos.dev/metal-installer/4b323f1e66fa76ba7bcb3eff14334ac5f37ce033c4514271c562f10bab63ce9a:v1.13.5

talos/common/cluster.yaml (Disable Flannel + kube-proxy)

Disables Flannel CNI (allowing Cilium to manage networking in full eBPF native routing mode), disables the legacy kube-proxy, and allows workloads to schedule directly on our single-node control plane.

# Cluster-wide settings for every single-node SBC cluster.
# CNI is disabled — Cilium is installed via Flux after bootstrap.
# kube-proxy is disabled — Cilium runs in full kubeProxyReplacement mode.
# Single node: the control-plane node must also run workloads.
cluster:
  allowSchedulingOnControlPlanes: true
  network:
    cni:
      name: none
    podSubnets:
      - 10.244.0.0/16
    serviceSubnets:
      - 10.96.0.0/12
  proxy:
    disabled: true

talos/common/storage.yaml (Pre-loaded Kernel Modules)

Pre-loads essential kernel modules for storage volume encryption and iSCSI initiators (keeping the node versatile for future block storage changes).

# Kernel modules for block storage (encrypted local volumes).
# Cheap to load and future-proofs the node; remove if you will never run dm_crypt volumes on the SBC.
machine:
  kernel:
    modules:
      - name: iscsi_tcp   # iSCSI initiator
      - name: dm_crypt    # Encrypted local volumes

2. Node-Specific Patch

talos/clusters/<node-name>/<node-name>.yaml (MAC Routing, DNS, and SANs)

This node-level overlay configures host DNS servers, binds the static IP reservation to your specific physical network interface (using hardwareAddr so kernel interface renames won’t break things), and configures Subject Alternative Names (SANs) for secure certificate access.

machine:
  features:
    hostDNS:
      enabled: true
      forwardKubeDNSToHost: false
  network:
    nameservers:
      - 192.168.0.1
    interfaces:
      - deviceSelector:
          hardwareAddr: "00:e0:4c:03:2b:bc"   # Onboard 2.5GbE NIC (RTL8125)
        dhcp: false
        addresses:
          - 192.168.1.5/22
        routes:
          - network: 0.0.0.0/0
            gateway: 192.168.0.1
  kubelet:
    nodeIP:
      validSubnets:
        - 192.168.1.5/32
  certSANs:
    - "192.168.1.5"
    - "kube.<node-name>.home.arpa"
    - "kube.<node-name>.internal"
cluster:
  apiServer:
    certSANs:
      - "192.168.1.5"
      - "kube.<node-name>.home.arpa"
      - "kube.<node-name>.internal"
---
# Talos v1.13 moved the hostname into a dedicated HostnameConfig resource.
apiVersion: v1alpha1
kind: HostnameConfig
hostname: <node-name>

3. Generate and Apply Configurations

With the board powered on and sitting in Talos Maintenance Mode, run the following commands from your workstation:

# Generate the base configurations pointing to your static node IP
talosctl gen config sbc-cluster https://192.168.1.5:6443 \
  --kubernetes-version v1.34.9 \
  --install-disk /dev/nvme0n1

# Apply common patches + node-specific patch to the node
talosctl apply-config --insecure --nodes 192.168.1.5 \
  --file controlplane.yaml \
  --config-patch @talos/common/cluster.yaml \
  --config-patch @talos/common/install.yaml \
  --config-patch @talos/common/storage.yaml \
  --config-patch @talos/common/lldpd.yaml \
  --config-patch @talos/common/ccm.yaml \
  --config-patch @talos/common/oidc.yaml \
  --config-patch @talos/clusters/<node-name>/<node-name>.yaml

# Trigger the bootstrap sequence on the node
talosctl --talosconfig ./talosconfig -n 192.168.1.5 bootstrap

# Download your kubeconfig and check node status
talosctl --talosconfig ./talosconfig -n 192.168.1.5 kubeconfig ~/.kube/sbc-node
kubectl get nodes

:hammer_and_wrench: Phase 6 — GitOps Bootstrap (Flux CD)

Now we hand control over to Flux. Because many home routers block outbound SSH on port 22, we bootstrap Flux using HTTPS Token Authentication:

export GITEA_TOKEN=<your-gitea-token>

flux bootstrap gitea \
  --components-extra=image-reflector-controller,image-automation-controller \
  --hostname=git.your-domain.com \
  --token-auth=true \
  --owner=<your-username> \
  --repository=Kubernetes-SBC \
  --branch=main \
  --path=./clusters/<node-name> \
  --personal \
  --private

Once Flux is bootstrapped, inject your SOPS decryption keys to handle secrets natively:

kubectl -n flux-system create secret generic sops-age \
  --from-file=age.agekey=/path/to/age.agekey

flux reconcile kustomization flux-system --with-source

:open_file_folder: GitOps Directory Layout

This monorepo pattern allows our fleet to scale. The core configurations stay in infrastructure/base/ (cert-manager, Cilium, local-path), while each individual SBC overlay is lightweight:

kubernetes-sbc/
├── clusters/
│   └── <node-name>/           # Single-Node Flux Entrypoint
│       ├── applications.yaml
│       └── infrastructure.yaml
├── infrastructure/
│   ├── base/                  # Shared base for ALL single-node SBCs
│   │   ├── sources/           #   HelmRepositories
│   │   ├── cilium/            #   CNI + Gateway API + L2 Announcements
│   │   ├── cert-manager/
│   │   └── local-path/        #   Lightweight Local NVMe Storage
│   └── <node-name>/           # SBC Ingress Overlay
│       ├── gateway.yaml       #   Envoy Gateway instance
│       ├── certificate.yaml   #   Wildcard TLS (*.<node-name>.home.arpa)
│       └── ip-pool.yaml       #   Cilium IPPool allocating 192.168.1.6
└── talos/
    ├── common/                # Shared board patches
    └── clusters/
        └── <node-name>/       # SBC hardware configuration (MAC, Hostname, IP)

:bullseye: Verification & Ingress Testing

Verify that Cilium has successfully claimed your LoadBalancer IP and announced it via ARP onto your home LAN:

kubectl get svc -A
# You should see your gateway-listener service assigned to the external IP: 192.168.1.6

Test the ingress directly from your workstation:

curl -k https://192.168.1.6
# Should hit the Envoy gateway (returning a 404 or certificate route redirect)

:link: The full stack at a glance

Layer Component
OS Talos Linux v1.13+ (immutable, API-only, no SSH)
Kubernetes v1.34.9 — single control-plane + worker on one node
CNI Cilium — eBPF, full kube-proxy replacement
LoadBalancer Cilium L2 Announcements — ARP-advertised LoadBalancer IP on the home network
Ingress Gateway API (Cilium native) — Envoy-backed, no deprecated ingress-nginx
Storage Local Path Provisioner — Lightweight, dynamic NVMe storage with zero replication tax
Secrets SOPS + age, encrypted in Git
GitOps Flux + image automation

:camera: Gallery


:folded_hands: Thank you, @radxa

This wouldn’t be possible without the Rock 5B. The combination of a native 2.5 GbE, a real PCIe 3.0 M.2 2280 slot, 32GB LPDDR4, an optional PoE+ module and — critically — a first-class Talos Linux overlay maintained alongside upstream releases makes this board a perfect Single-Node Cluster.


I’m happy to answer questions about reproducing any part of this and hoping for feedback and suggestions :raising_hands: