Bare-metal Kubernetes · two nodes, one board

deadc0de.cafe

Two Raspberry Pi compute modules on one Mini-ITX board, running an immutable Linux with no shell and an eBPF datapath with no kube-proxy.

TALOS  v1.14.0
K8S    v1.37.0
CILIUM 1.20.1
NODES  2 × CM4

The hardware

A Turing Pi 2 is a Mini-ITX carrier board with four compute-module sockets. Two are populated with Raspberry Pi CM4s — four Cortex-A72 cores and 8 GB of RAM each, booting Talos from 30 GB of on-module eMMC. Storage lives off-board on a separate NAS.

Freedom Internet native dual-stack UniFi Dream Machine SE 192.168.1.1 · DHCP .30–.254 · v6 firewall USW Pro Max 24 2.5 GbE GbE Turing Pi 2 Mini-ITX · 4 sockets, 2 populated cm4-1 192.168.1.235 holds the VIP cm4-2 192.168.1.58 etcd leader Kubernetes control plane — both nodes Talos Linux · etcd quorum 2 of 2 · no workers socket 3 — empty socket 4 — empty BMC · turingpi.lan · 192.168.1.72 pixy Ubuntu · 192.168.1.47 29 TB LVM · 14 TB free NFSv4 exports Plex + deluge (moving to cluster) NFS
One board, two nodes, storage kept separate. Both CM4s are control-plane members, so etcd quorum is two of two — losing a node stops the API, though workloads on the survivor keep serving. Two sockets sit empty; a third module is the single biggest upgrade available.

Ingress — two paths, one gateway

The interesting part is that IPv4 and IPv6 arrive by completely different mechanisms and only meet at the gateway. There is no cloud load balancer and no BGP, so the v4 address floats between nodes by ARP; v6 has no floating address at all and is fronted by a proxy pinned to each node.

IPv4 client A → 45.142.16.150 IPv6 client AAAA → 2a10:…:9709 :443 VIP 192.168.1.6 Cilium L2 announcement ARP · 15s lease · floats :443 node GUA hostNetwork nginx stream TCP proxy · TLS passthrough forwards to the VIP Cilium Gateway Envoy · Gateway API terminates TLS Let's Encrypt · cert-manager HTTPRoute by hostname img.deadc0de.cafe Immich · Google OIDC + ML: CLIP, faces, OCR nginx.deadc0de… nginx + oauth2-proxy email allowlist DATAPATH eBPF · kube-proxy replaced · native routing, no VXLAN · masquerade in BPF · pod MTU 1500
The v6 path has no floating address. IPv4 gets failover for free — the VIP moves to whichever node holds the lease. IPv6 is published per node, so DNS has to track cluster membership, and a decommissioned node must leave DNS before it leaves the rack.

At rest

Compute
8 cores2 × 4× Cortex-A72
Memory
16 GB~37% in use
Load
~30%idle, both nodes
Pods
30across 8 namespaces
etcd
58 MB2 members, in sync
Uptime
396 dnode age

Software

Talos LinuxImmutable OS — no shell, no SSH, no package manager. Configured entirely over an API; the whole node is a signed image.v1.14.0
KubernetesBoth nodes are control-plane. No workers yet.v1.37.0
CiliumeBPF datapath replacing kube-proxy, plus Gateway API and the ARP announcements that float the VIP.1.20.1
CloudNativePGPostgres 17 on a VectorChord image — Immich stores CLIP embeddings as vectors, which stock Postgres can't index.1.30.0
ImmichSelf-hosted photos. Machine learning runs on-node: CLIP for search, buffalo_l for faces, PP-OCRv5 for text.v3.1.0
cert-managerLet's Encrypt via HTTP-01, wired into the Gateway rather than an Ingress.v1.21.1
csi-driver-nfsStatic volumes onto pixy's exports — the pseudo-root is read-only, so nothing is dynamically provisioned.4.13.4

Details worth knowing

No SSH anywhere. Talos has no shell to log into. Everything — upgrades, config, etcd snapshots — goes through a gRPC API authenticated by client certificate. Those certs expire annually, which is its own adventure.

The CM4s need a special build. Upgrades must use an Image Factory installer carrying the Raspberry Pi overlay. The generic installer boots fine on x86 and leaves a CM4 without firmware.

Photos never touch the nodes. The 30 GB eMMC holds only the OS and container images; the library is NFS on pixy, mounted straight into the pods.

Backups run to the NAS. Nightly pg_dump and an etcd snapshot, kept 14 deep — enough to rebuild the cluster from the config repo.