The K3s cluster has a rack neighbour: a NAS hosting the media stack and Nextcloud, outside the cluster, on Docker Compose. The incumbent — an i5-4690 from 2014, 84W of TDP — is running out of breath. Its replacement is called gbnas: a silent build around an ASRock N100M board, designed for three roles — NAS, backup target for the cluster, and workstation for the agent that drives the infrastructure.
This part 1 covers the build and the first integration: bringing an x86 machine into the monitoring ecosystem of an ARM cluster. The migration of the media stack is covered in part 2.
The build
| Component | Choice | Why |
|---|---|---|
| Motherboard | ASRock N100M (mATX) | Integrated Intel N100, 4 cores, 6 W TDP, QuickSync iGPU |
| Case | Jonsbo N4 | 6× 3.5” bays + 2× 2.5” — 3 free bays to grow into |
| PSU | be quiet! SFX Power 3 450W | Silent, very generous headroom |
| Disks | WD Red Plus 6 TB (CMR) | mdadm RAID1 mirror (~5.5 TiB usable), free bays to grow into |
| SSD | 256 GB NVMe (OS) + 500 GB SATA (Docker apps) | Separate OS / appdata / data |
| Network | 2.5GbE PCIe card | link verified at 2500 Mb/s |
| Cooling | Noctua NF-A12x25 + NF-A6x15 | inaudible |
| SATA controller | StarTech PCIe x1 | the N100M only has 2 native SATA ports |
Two quirks of the N100M worth knowing: a single DDR4 SO-DIMM slot (16 GB max), and only 2 SATA ports — hence the PCIe controller. Both PCIe slots are therefore taken (network + SATA), leaving only the M.2.
Compared with the outgoing machine: equivalent Passmark (~5500 vs ~5100) for 14× less TDP (6 W vs 84 W), plus a modern QuickSync that decodes AV1 — Jellyfin transcoding will happen in hardware.
OS: Ubuntu Server, without agonising over it
I weighed TrueNAS Scale (native ZFS, but ~1 GB of RAM per TB, and a 16 GB ceiling here), Proxmox (overkill for a single-purpose NAS) and Unraid (licence). Ubuntu Server won on pragmatism: the current stack is pure Docker Compose — the migration will be an rsync plus a docker compose up, not a conceptual reinstall.
Setup as built: static IP 10.0.0.30, Docker + Compose, and the 2.5GbE link validated with iperf3 at a steady 2.37 Gbit/s towards the PC. The internet speedtest tops out around 1 Gbit/s symmetrical — that is the subscription talking, not the machine.
The monitoring integration: just another scrape target
The reflex to avoid: installing a monitoring stack on the NAS (a second Grafana, a local Netdata…). The NAS is not in the cluster, but it has no reason to own its observability either: vmagent scrapes whatever it is told to scrape, inside the cluster or outside it.
job vmagent en plus (instance="gbnas"), et l'invité x86 apparaît dans les mêmes
dashboards que les Pi. SMART inclus via smartctl_exporter (température et santé des disques). The NAS is not in the cluster — it's a scrape target like any other :
one more vmagent job (instance="gbnas"), and the x86 guest shows up in the same
dashboards as the Pis. SMART included via smartctl_exporter (disk temperature and health). node_exporter — 10 lines
One container on the NAS, one static job on the vmagent side:
# monitoring/vm/vmagent-values.yaml
- job_name: nas-gbnas
static_configs:
- targets: ['10.0.0.30:9100']
labels:
instance: gbnas
role: nas
Immediate result: gbnas shows up in the “Benchmarks — all machines” dashboard alongside the 6 Pis. CPU, RAM, disk, network, temperatures — same panels, same alerts available. During a network audit the week before, the NAS was literally invisible in the measurements; that hole is plugged.
Portainer — the agent, not a second Portainer
For the NAS containers (the future media stack), Portainer — which runs in the cluster — already knows how to manage several environments. One agent on the NAS:
services:
portainer_agent:
image: portainer/agent:2.42.0
restart: always
ports: ["9001:9001"]
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
A topology subtlety: standard agent, not Edge Agent. The standard agent gets contacted by Portainer (pod → 10.0.0.30:9001) — trivial on the same LAN. The Edge Agent reverses the direction (NAS → Portainer tunnel), which would mean exposing a port through Cloudflare. On a LAN: standard, and port 9001 stays strictly internal (it exposes the Docker socket — never through the tunnel).
smartctl_exporter — and the ARM64 trap
Mechanical disks that will spin 24/7 deserve better than a quarterly manual smartctl -a. smartctl_exporter exposes temperature, power-on hours, reallocated sectors and SMART status — on the NAS and on the SSDs of the cluster nodes (privileged DaemonSet, /dev access).
The trap that cost an evening: since v0.7.0, the officially published image is amd64-only. On the Pis: exec format error, on repeat. The fix — pin by digest the last multi-arch image:
# The "latest" tag lies about its architectures. The digest never does.
# (digest truncated below for readability)
image: quay.io/prometheuscommunity/smartctl-exporter@sha256:b71402…
It is the same reflex as digest pinning in CI, applied for a different reason: not reproducibility, but architectural truth. On a mixed ARM/x86 fleet, a floating tag can change the architectures it supports under your feet.
The alerts that come with it
Wiring the NAS into monitoring is not just cosmetic — it inherits the cluster’s alerting pipeline. Next up, rules dedicated to the disks:
- alert: NasDiskTemperature # smartctl_device_temperature > 50°C
- alert: NasDiskUnhealthy # smartctl_device_smart_status != 1
- alert: NasDiskAlmostFull # the RAID1 mirror > 85%
What comes next (part 2)
The migration has happened — and the plan did not survive contact with reality intact (spoiler: RAIDZ1 lost against an mdadm mirror). The full media stack, Nextcloud and two surprise guests now run on gbnas: part 2 — the media stack moves in.
After that come the projects where the NAS actually serves the cluster: Longhorn backups offloaded from worker-2’s precious SSD, an out-of-cluster backup CronJob, an NFS StorageClass.