❯ infra · Running

Un NAS x86 à 6W rejoint le cluster Pi — partie 2 : la stack media déménage A 6W x86 NAS Joins the Pi Cluster — Part 2: The Media Stack Moves In

Migration de 12 conteneurs Docker — Jellyfin, la suite *arr, Nextcloud et compagnie — d'un vieil i5 à 84W vers le nouveau NAS N100 : rsync, une décision RAID1 pragmatique qui a battu le plan RAIDZ1, une bascule de routes Cloudflare Tunnel, et zéro interruption visible. Migrating 12 Docker containers — Jellyfin, the *arr suite, Nextcloud and friends — from an aging 84W i5 to the new N100 NAS: rsync, a pragmatic RAID1 decision that beat the RAIDZ1 plan, a Cloudflare Tunnel route flip, and zero user-visible downtime.

· 8 min de lecture 8 min read ·
nasdockermigrationjellyfinraidcloudflareinfra
🇫🇷 Lire cet article en français

Part 1 ended with a NAS that was assembled, instrumented, and empty. Here is what followed: the full media stack — Jellyfin, the *arr suite, qBittorrent, Nextcloud — moved from the old i5 (84 W, 2014) to gbnas (6 W). As I write this, 12 containers have been running for more than a week without a single incident.

The plan lost against pragmatism: RAID1, not RAIDZ1

Part 1 proudly announced a RAIDZ1 across 3× 6 TB (~12 TB usable). What actually got installed:

sdc                5.5T  disk
└─md127            5.5T  raid1  /mnt/storage
sdd                5.5T  disk
└─md127            5.5T  raid1  /mnt/storage

A 2-disk mdadm mirror, ~5.5 TiB usable. No ZFS. What tipped the balance:

  • mdadm is boring — in the noble sense: twenty years of production, built into the kernel, cat /proc/mdstat and you are done diagnosing. It was already the technology of the old NAS: zero mental migration.
  • ZFS and its appetite: the rule of thumb of ~1 GB of RAM per TB managed sits badly with the N100M’s 16 GB ceiling (a single SO-DIMM slot). Between a starving ARC and RAM for the containers, I chose the containers.
  • Two disks in service, empty bays: the Jonsbo N4 has 6 bays. Starting with a simple mirror and growing later (or switching to a real RAIDZ when both the need and the disks are there) is more honest than tying up three disks for a current need of ~2 TB.

The meta lesson: an infrastructure plan is a hypothesis, not a commitment. Documenting the actual choice and its reasons beats bending reality to match the published plan — which is exactly why this part 2 exists.

The final storage split: a 256 GB NVMe for the OS (LVM), a 500 GB SATA SSD mounted on /mnt for the Docker application volumes (configs, SQLite databases, metadata), and the 5.5 TiB mirror on /mnt/storage for the data.

The migration itself: the Docker Compose advantage

This is where part 1’s “Ubuntu Server + Docker Compose” choice pays off. Migrating a Compose stack is three operations with no magic involved:

  1. rsync -avP of the media data and the appdata folders — the Sonarr/Radarr SQLite databases, the Jellyfin metadata, everything travels with the files. Nothing to export, nothing to rebuild.
  2. Adapt the paths in the docker-compose.yml (mounts point to /mnt/storage and the appdata SSD) and docker compose up -d.
  3. Flip the routes: the public hostnames (jellyfin.*, qbit.*, nextcloud.*…) are defined in the Cloudflare Tunnel dashboard and used to point at the old NAS’s IP. Change the target to 10.0.0.30 — the public URLs, the certificates and the access rules do not move a pixel.

The result on the docker ps side, a week later:

jellyfin      lscr.io/linuxserver/jellyfin      Up 9 days
sonarr        lscr.io/linuxserver/sonarr        Up 9 days
radarr        lscr.io/linuxserver/radarr        Up 9 days
bazarr        lscr.io/linuxserver/bazarr        Up 9 days
prowlarr      lscr.io/linuxserver/prowlarr      Up 9 days
qbittorrent   lscr.io/linuxserver/qbittorrent   Up 9 days
flaresolverr  flaresolverr/flaresolverr         Up 9 days
nextcloud     lscr.io/linuxserver/nextcloud     Up 8 days

A bonus from the extra horsepower: the N100 brings a modern QuickSync (AV1 decoding, HEVC encoding) — Jellyfin transcoding now happens in hardware on the iGPU, where the old i5 was grinding through it in software on its heavier profiles.

The surprise guests

A comfortable machine attracts workloads. Two services joined the stack without being in the original plan:

  • Home Assistant — home automation deserved better than a precarious existence; it now has a stable home port
  • Bookshelf (reading tracker, Hardcover flavour) — the kind of small service you never deploy “because there is nowhere to put it”

That is a second-order effect of the 6 W: the machine runs 24/7 without energy guilt, so every new service has a marginal cost of zero.

The monitoring loop is closed

Everything wired up in part 1 now covers a machine that actually does something: node_exporter reports the RAID1 mirror’s I/O into Grafana (the NAS disk shows up in the cluster audits), smartctl_exporter watches the health of the disks that now carry all the data, and the Portainer agent (bumped to 2.42.0 along the way) gives the container view alongside the K3s workloads.

The old NAS is powered off. 84 W → 6 W continuously, which is roughly 680 kWh saved per year — the kind of number that makes a homelab defensible in the kitchen.

What is left (the real part 3)

The NAS hosts, but it does not yet serve the cluster:

  • Backup offload: move the S3 target of the Longhorn backups (MinIO, today 200 Gi on worker-2’s precious SSD) to the NAS mirror
  • backup.sh → K8s CronJob pushing the dumps out of the cluster, to /mnt/storage/backups
  • An NFS StorageClass to offer the cluster capacity-oriented storage alongside Longhorn
  • The UPS question: mechanical disks carrying everything do not like hard power cuts

That will be part 3 — when it is actually done, not before. I learned the RAIDZ1 lesson.

nasdockermigrationjellyfinraidcloudflareinfra