Retiring the QNAP TS-873: How I Built a Silent 92TB Unraid 7 NAS with 10GbE & Local AI

Retiring the QNAP TS-873: How I Built a Silent 92TB Unraid 7 NAS with 10GbE and Local AI

For several years, my 8-bay QNAP TS-873 handled everything I threw at it: NFS shares, Time Machine backups, Jellyfin media, and a Home Assistant virtual machine. Eventually, the hardware hit a wall. The embedded AMD RX-421BD quad-core CPU choked during heavy I/O, QTS firmware updates took twenty minutes to reboot, all eight 3.5-inch drive bays were full, and the proprietary chassis had neither the physical clearance nor the PCIe power delivery to host a full-height NVIDIA GPU for local AI workloads.

In today’s post, I will walk you through how I reused the AMD Ryzen 7 5800X, ASUS ROG STRIX B550-E GAMING motherboard, and 32 GB of DDR4-3600 RAM left over from my recent AM5 workstation upgrade to build a silent, 12-drive Unraid OS 7 server with a 4 TB ZFS SSD mirror, 10GbE networking, KVM virtualization, and an NVIDIA RTX 3060 12GB for local CUDA inference.

Step 1: Hardware Architecture & PCIe Slot Layout

Repurposing desktop components into a 24/7 storage server works well only if you plan your PCIe lanes, SATA controllers, and power delivery ahead of time.

Component Model / Specification Role in the NAS
Chassis Fractal Design Define 7 PCGH Edition Sound-dampened full tower supporting up to 14× 3.5" HDD trays
CPU AMD Ryzen 7 5800X (8C / 16T, Zen 3) Compute for ZFS, KVM (HAOS), and high-speed parity calculations
CPU Cooler Noctua NH Tower Cooler Near-silent thermal dissipation under continuous load
Motherboard ASUS ROG STRIX B550-E GAMING Splits CPU lanes into PCIe 4.0 x8 / x8 across Slots 1 & 2
Memory 32 GB (2× 16 GB) DDR4-3600 Host OS, ZFS ARC cache, KVM VM, and RAM transcoding
GPU (Slot 1) NVIDIA GeForce RTX 3060 12GB (Dual-Slot) Local AI inference (Ollama, Wyoming Whisper STT & Piper TTS)
10GbE NIC (Slot 2) Intel X540-AT2 Dual-Port 10GBase-T 10GbE uplink to the core switch
SATA HBA (Slot 4) ASM1166 6-Port SATA III 6Gb/s (PCIe x4) Expands onboard 6× SATA to 12× SATA 6Gb/s ports (all 12 populated)
ZFS Cache Pool 2× SanDisk Ultra 3D 4 TB SSD 4 TB ZFS Mirror (/mnt/cache) for appdata, KVM domains, and system
Hard Drives 10× 3.5" HDDs (4× 16 TB, 4× 12 TB, 1× 8 TB, 1× 4 TB) 92 TB usable XFS data array (8 data drives) + Dual 16 TB Parity
Power Supply be quiet! Pure Power 12 750W 80+ Gold (ATX 3.0) Single high-current +12V rail for 10+ HDD spin-up surges + GPU
UPS APC Back-UPS BX1200MI (1200 VA / 650 W) USB apcupsd master protecting NAS, UDM Pro router, and MS-01 host

Fractal Design Define 7 PCGH in Storage Layout with stacked 3.5-inch HDD trays

Fitting Both a GPU and a 10GbE Card on a B550 Motherboard

Running a dedicated GPU alongside a 10GbE network card and a 6-port SATA expander on a consumer AM4 board requires careful attention to how the motherboard routes PCIe lanes. Cheap B550 motherboards wire the primary slot (PCIEX16_1) to the CPU at x16 and route the second full-length slot (PCIEX16_2) through the B550 chipset at PCIe 3.0 x4, which often disables onboard SATA ports or the second M.2 slot when populated. The ASUS ROG STRIX B550-E GAMING avoids this by splitting the 16 PCIe 4.0 lanes from the Ryzen 7 5800X directly into PCIe 4.0 x8 (PCIEX16_1) + PCIe 4.0 x8 (PCIEX16_2). That gives the RTX 3060 12GB PCIe 4.0 x8 bandwidth in Slot 1 while giving the Intel X540-AT2 10GbE card dedicated CPU lanes in Slot 2 without touching chipset I/O.

The bottom full-length slot (PCIEX16_3, wired to the chipset at PCIe 3.0 x4) hosts the ASM1166 6-port SATA controller. Unlike older enterprise SAS HBAs such as the LSI 9211-8i that burn 12–15W at idle and block deeper PCIe ASPM power states, the ASM1166 draws under 2W and works out of the box with the Linux ahci driver.

Gotcha: Check the physical thickness of your GPU before buying! On the B550-E, PCIEX16_1 and PCIEX16_2 sit three slots apart. A 2.5-slot or 3-slot gaming GPU physically covers the second x16 slot, leaving nowhere to plug in the 10GbE card. Stick to a strict 2.0-slot card, and mount an auxiliary fan across the lower PCIe zone because server NICs like the Intel X540-AT2 expect rackmount airflow and run very hot in a silent tower case.

ASUS ROG STRIX B550-E GAMING with Noctua push-pull cooler, Intel X540-AT2 10GbE NIC in Slot 2, and ASM1166 6-port SATA HBA in Slot 4 before dropping in the Dual-Slot RTX 3060


Step 2: Storage Layout & The “Parity-Last” Migration Strategy

Moving away from QNAP’s rigid RAID groups to Unraid 7 let me combine all 10 hard drives (4× 16 TB, 4× 12 TB, 1× 8 TB, and 1× 4 TB) into a single pool protected by Dual Parity, backed by a 4 TB ZFS cache mirror (2× SanDisk 4 TB SSD) across all 12 SATA ports:

Unraid 7.3.4 Main tab showing the 10-drive XFS array with Dual 16TB Parity and the 2x SanDisk 4TB ZFS Mirror cache pool

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
┌──────────────────────────────────────────────────────────────────────────────┐
│                        UNRAID 7.3.4 STORAGE ARCHITECTURE                     │
├──────────────────────────────┬───────────────────────────────────────────────┤
│ TIER / POOL                  │ CONFIGURATION & WORKLOADS                     │
├──────────────────────────────┼───────────────────────────────────────────────┤
│ Cache Pool (/mnt/cache)      │ 2× SanDisk 4 TB SSD (4 TB ZFS Mirror)         │
│   ├─ /mnt/user/appdata       │ Docker container configs & databases          │
│   ├─ /mnt/user/domains       │ KVM Virtual Machines (Home Assistant OS qcow2)│
│   └─ /mnt/user/system        │ Docker vDisk & libvirt images                 │
├──────────────────────────────┼───────────────────────────────────────────────┤
│ Main Data Array (/mnt/user)  │ 8× XFS Data Drives (92 TB Usable, High Water) │
│   ├─ Parity 1 + Parity 2     │ 2× 16 TB WD Red Pro (Dual Parity Protection)  │
│   └─ Data Disks 1 .. 8       │ 2× 16 TB, 4× 12 TB, 1× 8 TB, 1× 4 TB HDDs     │
└──────────────────────────────┴───────────────────────────────────────────────┘

Migrate Your Bulk Data Before Assigning Parity Drives

Note: Do not assign Parity 1 and Parity 2 before your initial data migration! If you enable parity on day one and start copying 60+ TB of data into the Unraid array, every write triggers a read-modify-write parity calculation across the spinning disks and throttles your transfer speeds to 60–90 MB/s.

Instead, run the migration in two steps:

  1. Stage 1 (Unprotected Bulk Ingest): Assign only the 8 XFS data drives (Disk 1 through Disk 8) with no parity drives installed. Run background rsync jobs over the 10GbE network from the old QNAP NAS and from external USB 3.2 staging drives (22 TB Seagate Exos + 14 TB WD Red Pro mounted via the Unassigned Devices plugin):
    1
    2
    
    rsync -avh --no-inc-recursive --info=progress2 \
      /mnt/disks/USB_Staging_22TB/Media/ /mnt/user/Media/
    
    Without parity overhead, writes saturate the native mechanical speed of each target drive (180–240 MB/s per disk).
  2. Stage 2 (Enable Dual Parity): Once the initial multi-terabyte ingest finishes, stop the array, assign both 16 TB WD Red Pro drives to Parity 1 and Parity 2, and start the array. Unraid builds Dual Parity sequentially from outer to inner cylinders at 210+ MB/s in a single pass.

Step 3: 10GbE Networking & Fixing NFSv4.2 Stale File Handle Errors

My compute layer runs on a separate Minisforum MS-01 server (Intel Core i9, 96 GB RAM, CachyOS) hosting 70+ Docker containers, while bulk media and backups live on the Unraid NAS. Both servers connect via 10GbE (Intel X710-DA2 on the MS-01 and Intel X540-AT2 on Unraid) into the core 10GbE switch.

Sequential disk benchmarks over NFSv4.2 confirmed the storage stack was performing at full speed:

1
2
3
4
5
6
7
# 1. Direct NFSv4.2 Sequential Write (Bypassing RAM Cache)
$ dd if=/dev/zero of=/mnt/nas_share/test_10g.bin bs=1M count=4096 oflag=direct
4294967296 bytes (4.3 GB, 4.0 GiB) copied, 6.96091 s, 617 MB/s

# 2. Cached Read Throughput (Linux Page Cache + ZFS ARC)
$ dd if=/mnt/nas_share/test_10g.bin of=/dev/null bs=1M
4294967296 bytes (4.3 GB, 4.0 GiB) copied, 0.394112 s, 10.9 GB/s

Fixing ESTALE (errno -116) & I/O Freezes Under Heavy Load

The dd numbers looked great right until I ran a multi-terabyte background rsync while Immich was generating machine-learning thumbnails and Jellyfin and Sonarr were scanning libraries over NFSv4.2 from the MS-01 Docker host. Containers started crashing with:

1
OSError: [Errno 116] Stale file handle

At the same time, directory listings (ls /mnt/nas_share) on the Docker host would freeze for 15–20 seconds. I spent several hours digging through Unraid (6.12 LTS kernel) and shfs behaviour to track down all three root causes so you don’t have to:

  1. Unraid shfs inode eviction (fuse_remember=330): When you export /mnt/user/<share> over NFS, clients talk to rpc.nfsd on top of Unraid’s FUSE filesystem (/usr/libexec/unraid/shfs). By default, shfs caches inode mappings in RAM for only 330 seconds (-o remember=330). Under heavy file-scanning load from Immich and Jellyfin, shfs evicts older inode entries from memory. The moment a remote NFSv4.2 client references an evicted file handle, shfs fails to resolve the inode and returns ESTALE.
  2. Linux dirty page writeback stalls (vm.dirty_ratio=20): On a server with 32 GB of RAM, Linux’s default vm.dirty_ratio=20 and vm.dirty_background_ratio=10 allow the kernel to buffer up to 6.4 GB of uncommitted writes in RAM during an rsync job before forcing synchronous writeback. Flooding mechanical HDDs with a 6.4 GB flush blocks shfs and nfsd worker threads for 20+ seconds.
  3. Client-side actimeo=1800 caching: Many homelab guides suggest adding actimeo=1800 to /etc/fstab to reduce metadata lookups. On a media server, caching directory attributes for 30 minutes means Sonarr, Radarr, and Jellyfin cannot see newly downloaded files created on the NAS for half an hour.

The 4-Step Fix

  1. Disable shfs Inode Eviction & Enable Exclusive Shares: In /boot/config/share.cfg on the Unraid USB flash drive, set fuse_remember to -1 (never evict cached inodes until array stop), raise the file descriptor limit for shfs to 524288, and enable Exclusive Shares. Cache-only ZFS shares (appdata, domains, isos, system, backup) then use direct kernel symlinks (/mnt/user/appdata -> /mnt/cache/appdata, shown as ▸ Cache in the Unraid UI) and bypass FUSE completely, while bulk media and backup shares (Cache ➔ Array) stage incoming writes on the ZFS SSD pool before the nightly Mover flushes them to the XFS array:

    Unraid 7.3.4 User Shares showing Exclusive Shares (▸ Cache) for appdata, domains, and system alongside Cache -> Array staging for NFS media shares

    1
    2
    3
    4
    
    # /boot/config/share.cfg
    shareUserExclusive="yes"
    shareFuseRemember="-1"
    shareNOFILE="524288"
    

    After restarting the array, verify that shfs is running with -o remember=-1:

    1
    2
    
    root@NAS:~# ps aux | grep shfs
    root  14210  2.1  0.4 845120 148200 ?  Ssl 12:14  0:42 /usr/libexec/unraid/shfs /mnt/user -disks 255 -o default_permissions,allow_other,noatime -o remember=-1
    
  2. Clamp Kernel Dirty Page Writeback in /boot/config/go: To force the Linux kernel to flush writes continuously in smaller batches (~600 MB background, ~1.2 GB hard ceiling) instead of stalling the bus with multi-gigabyte flushes, persist sysctl rules in /boot/config/go:

    1
    2
    3
    4
    5
    6
    7
    
    #!/bin/bash
    # Start the Management Utility
    /usr/local/sbin/emhttp
    
    # Tune Linux writeback cache for smooth NFSv4.2 / shfs latency
    sysctl -w vm.dirty_background_ratio=2
    sysctl -w vm.dirty_ratio=4
    
  3. Automate Unraid 7 Settings via /var/run/emhttpd.socket: If you manage your server via SSH or Ansible, trying to POST configuration changes to Unraid’s Nginx web interface (http://localhost/update.htm) fails unless you scrape session cookies and CSRF tokens. In Unraid 7, emhttpd listens directly on a local Unix domain socket at /var/run/emhttpd.socket. You can apply system changes or trigger array actions from the CLI with curl --unix-socket:

    1
    2
    3
    4
    
    # Programmatically updating a share or system setting via emhttpd Unix socket
    curl --unix-socket /var/run/emhttpd.socket \
      http://localhost/update.htm \
      -d "shareFuseRemember=-1&shareUserExclusive=yes&changeShare=Apply"
    
  4. Configure Clean Client /etc/fstab Mount Options on the Docker Host: On the Minisforum MS-01 Docker host, mount the Unraid exports using NFSv4.2 with 1 MB read/write blocks (1048576), async, hard, noatime, and without actimeo:

    1
    2
    3
    
    # /etc/fstab on Docker Host (MS-01)
    10.10.255.50:/mnt/user/Media   /mnt/nas_media   nfs4  nfsvers=4.2,rw,async,hard,intr,noatime,rsize=1048576,wsize=1048576,_netdev  0  0
    10.10.255.50:/mnt/user/Backup  /mnt/nas_backup  nfs4  nfsvers=4.2,rw,async,hard,intr,noatime,rsize=1048576,wsize=1048576,_netdev  0  0
    

    Verify the active kernel mount flags with nfsstat -m to confirm vers=4.2,rsize=1048576,wsize=1048576 are negotiated. Since applying these four changes, I haven’t seen a single Stale file handle error, even during simultaneous 10GbE rsync migrations and Immich ML indexing.


Step 4: Home Assistant OS on KVM, Live fsfreeze Backups & Offsite Cloud Sync

On the old QNAP TS-873, my Home Assistant OS (HAOS) virtual machine ran inside QNAP Virtualization Station with noticeable UI lag during database purges. Migrating it to Unraid took less than fifteen minutes:

  1. Copied the qcow2 disk image directly onto the 4 TB ZFS Cache Mirror (/mnt/cache/domains/haos/vdisk1.qcow2).
  2. Created a KVM virtual machine (OVMF UEFI, VirtIO network bound to br0 with its static VLAN IP, and USB passthrough for the Sonoff Zigbee 3.0 USB Dongle Plus 10c4:ea60).
  3. Paired with local RTX 3060 12GB containers on Unraid (wyoming-whisper for speech-to-text, wyoming-piper for text-to-speech, and ollama for local LLM intent processing), Home Assistant voice pipelines now respond in milliseconds while the VM idles at a load average of 0.08.

Home Assistant OS (haos) KVM virtual machine running on the Unraid 7.3.4 ZFS cache pool

Zero-Downtime VM Backups (18 GB in 102 Seconds)

Instead of shutting down Home Assistant every week to back up its virtual disk, I configured the VM Backup plugin to use the QEMU Guest Agent (fsfreeze) and zstd multithreaded compression:

  • Before copying the vDisk, libvirt instructs the QEMU Guest Agent inside HAOS to freeze filesystem writes (fsfreeze-freeze), flushing SQLite and MariaDB transactions cleanly to disk.
  • Unraid streams and compresses the 18 GB qcow2 image using zstd in 1 minute 42 seconds (102s), then immediately thaws the filesystem (fsfreeze-thaw). Home Assistant stays online the entire time without dropping a Zigbee or MQTT packet.

Nightly Offsite Sync to Google Cloud Storage (rclone --size-only)

For offsite backups, an Unraid User Script runs every night at 03:30 to sync critical shares (Photos, Documents, Music, and encrypted archives) to a Google Cloud Storage (GCS) bucket protected by customer-managed encryption keys (CMEK):

1
2
3
4
5
6
7
8
9
#!/bin/bash
# Unraid User Script: Nightly Offsite Sync to GCS (03:30)
rclone sync /mnt/user/Photos gcs-offsite:homelab-backup-archive/Photos \
  --size-only \
  --transfers=8 \
  --checkers=16 \
  --bwlimit=50M \
  --log-file=/var/log/rclone-gcs-nightly.log \
  --log-level=INFO

By default, rclone compares checksums or modification timestamps, which can force local disk reads or slow metadata comparisons across hundreds of thousands of photo files. Passing --size-only cut the nightly pre-sync check from hours to a few seconds.


Step 5: The Shared APC UPS Setting That Almost Killed My Router (KILLDELAY 0)

During UPS failover testing, one setting in Unraid nearly took down my entire network. My APC Back-UPS BX1200MI powers three devices:

  1. UniFi Dream Machine Pro (UDM Pro) and core PoE switches
  2. Minisforum MS-01 Docker host
  3. Custom Unraid NAS (connected to the UPS via USB cable as the apcupsd master)

Because the Unraid NAS spins 10 mechanical hard drives, I configured it to start a clean shutdown early in a power outage, as soon as battery runtime drops below 15 minutes or battery charge hits 35%.

Gotcha: In Unraid’s Settings -> UPS Settings, watch out for “Turn off UPS after shutdown”. If you leave it set to Yes, Unraid instructs the APC UPS to cut AC output power (killpower) right after the NAS finishes shutting down. Because my UDM Pro and Minisforum MS-01 share that same UPS and draw far less power than 10 hard drives, sending killpower from Unraid cuts power to the router and Docker host while the battery still has 35% charge left! Always set Turn off UPS after shutdown: No (which writes KILLDELAY 0 in /etc/apcupsd/apcupsd.conf) when your NAS shares a UPS with network gear.


Summary

To sum it up, moving from the QNAP TS-873 to this custom Unraid 7 build solved every storage, noise, and compute bottleneck in my homelab. With all 10 drives mounted in the sound-dampened Fractal Define 7 PCGH and cooled by Noctua fans, the array sits at 37–39 °C and the SanDisk ZFS pool runs at 28–31 °C while remaining practically silent in the office.

Best of all, when the 92 TB array eventually fills up, I don’t need to rebuild a RAID group or buy a new appliance: I can just swap out the smallest disk (4 TB or 8 TB) for another 16 TB drive and let Unraid expand the filesystem in place.

Written from personal dictation & experience, edited with AI pair-programming tools.
Built with Hugo
Theme Stack designed by Jimmy