Unraid (Holly)¶
Overview¶
- Hostname: Holly
- IP: 192.168.1.200 (SSH on port 22159)
- URL: holly.mdhmedia.uk
- Role: Primary NAS, media storage, Docker host
- Platform: Unraid 7.2.5 — runs as VM 110 on pve2 (USB boot, PCI passthrough), not bare metal
- Containers: managed via Unraid Docker UI / XML templates (Compose Manager plugin installed but unused)
Fully recovered — 2026-07-29
The 2026-07-25 boot-USB failure (fixed by reseating the flash drive on
2026-07-26) had also corrupted docker.img; the Docker daemon came back
after a VM reboot on 2026-07-29 and Holly is fully operational again.
Backups¶
vzdump can't cover Holly (USB boot + passthrough disks — appdata lives on the
physical array, invisible to the hypervisor), so since 2026-07-31 a static
proxmox-backup-client runs inside Holly and ships appdata + the boot
flash to the PBS datastore as host/holly. Hand-deployed to
/boot/config/plugins/holly-pbs-backup/ (plus a dynamix cron file); the PBS
token/fingerprint live in a Holly-local pbs.env, not in git. See
holly/backup/
in the repo.
| Time | Step |
|---|---|
| 06:00 | Consistent staging copy — containers quiesced, appdata rsynced to a cache-SSD staging area |
| 20:35 | Upload of staging + /boot to host/holly on PBS, late in the WoL wake window (moved from 19:25 on 2026-08-03 — pve3 is load-marginal under concurrent load; the job exits quietly if PBS is asleep) |
The nightly PBS → Hetzner offsite sync carries host/holly along
automatically, and the first successful appdata backups are in the datastore
(the Jarvis 20:05 backup-watchdog tracks their age — ~23.5 h at check time is
healthy). Unraid Connect's cloud flash backup remains as the second,
independent copy of the boot flash.
Docker Containers¶
Inventory as of 2026-07-03 (all on the default bridge unless noted); versions are point-in-time and may lag the live Unraid Docker UI:
| Container | Version | Port | URL | Purpose |
|---|---|---|---|---|
| NZBGet | v26.2-ls250 | 6789 | nzbget.mdhmedia.uk | Usenet downloader |
| Prowlarr | 2.4.0.5397 | 9696 | prowlarr.mdhmedia.uk | Indexer manager |
| Radarr | 6.2.1.10461 | 7878 | radarr.mdhmedia.uk | Movie automation |
| Sonarr | 4.0.19.2979 | 8989 | sonarr.mdhmedia.uk | TV automation |
| qBittorrent | 5.2.1 | 8080 | torrent.mdhmedia.uk | Torrent client |
| Ombi | v4.53.10 | 3579 | ombi.mdhmedia.uk | Media requests |
| Plex | 1.43.2.10687 | 32400 | — (route commented out) | Media server — dedicated br0 IP 192.168.1.210 |
| Speedtest Tracker | v1.14.5 | 8084 | speedtest.mdhmedia.uk | Network speed tracking |
| Audiobookshelf | 2.35.1 | 13378 | abs.mdhmedia.uk | Audiobook server |
| Libation | retired 2026-08-10 | — | libate.mdhmedia.uk | GUI container retired; liberation now runs headless as libation-sync on CT 101 |
| iperf3 | — | host net | — | Throughput testing |
Not running: webtop (created, never started), abs-organiser (exited Apr 2026). Stale XML templates with no container behind them: GrafanaLoki, NginxProxyManager, cloudflared, cloudflareddns, jackett, netvisor-daemon.
Storage¶
Minimum Free Space — the setting that actually breaks writes¶
On a JBOD array each file lives whole on one disk, so a disk being 95% full does not stop writes: shfs routes the next file elsewhere. What does break a write is Minimum Free Space being smaller than the file being written — shfs starts the write on a disk that cannot finish it.
Audited and corrected 2026-08-10:
| Share | Was | Now | Why |
|---|---|---|---|
Media |
23.8 GiB | 100 GiB | 91 files exceed the old floor; the largest on the array is 83.2 GiB (Interstellar 00001.m2ts). An 83 GiB remux could be started on a disk with 25 GiB left |
onedrive |
0 | 20 GiB | A floor of 0 lets shfs fill a disk to zero mid-file. The share is empty today, so this was latent |
downloads |
372 GiB | unchanged | Looks absurd next to a 3.36 GiB largest file, but it is protective: it keeps a churning share off the nearly-full disks, confining it to disk3/disk5 |
Rebalance — 2026-08-11 (~460 GB moved, disk1 + disk4 → disk3)¶
Moved four whole TV-show folders (Frasier, House of the Dragon ×2, One Piece)
off the two fullest disks onto disk3, which had by far the most headroom.
Unbalance was installed for this,
but its scatter/gather engine turned out to be an undocumented WebSocket
protocol (scatter:plan:start / move / copy commands, no REST API) — not
something to reverse-engineer live against production data. The move was done
directly instead, with the same discipline: rsync -a --checksum to a
temporary name on the target disk → independent file-count + byte-count
verification → source removed only after verification passes → verified
copy renamed into its final place. Array health re-checked before and after
every item.
| Disk | Before | After |
|---|---|---|
| disk1 | 219 GB free | 371 GB free |
| disk3 | 1.3 TB free | 852 GB free |
| disk4 | 222 GB free | 503 GB free |
The mv-into-existing-directory pitfall
mv SRC DST where DST already exists as a directory nests SRC
inside it (DST/basename(SRC)) rather than merging — and does this
silently, with no error, if the nested name doesn't collide with
anything yet. That's what happened moving House of the Dragon off disk4:
the show already had unrelated content on disk3 (a pre-existing Season 3
and one Season 2 episode from the normal split-level allocator), so
$final already existed, and the disk4 portion landed nested one level
down without complaint. It only became visible when the second
House-of-the-Dragon move (from disk1) tried the same nested name and it was
now occupied - mv: cannot overwrite '.../House of the Dragon/House of the
Dragon': File exists.
Nothing was ever at risk (every deletion happens only after independent verification), but the final structure needed a manual, collision-checked merge: move the nested content's files into the flat path one season at a time, verifying no filename collision first.
A file's mtime does not prove when it arrived at a path - mv preserves
the original mtime, so old dates on files sitting somewhere unexpected do
not mean "pre-existing junk". Reconcile by name+size (or a checksum) against
what the move actually reported, not by how old the files look.
Any future script driving this kind of move must merge into an existing
target directory explicitly (mv SRC/* DST/, after a collision check)
rather than mv SRC DST and trust it either renames cleanly or errors.
How it was applied
The supported route is the webGUI (POST /update.htm), but an
unauthenticated POST 302s to /login. Instead both files emhttpd keeps in
sync were edited directly, with timestamped backups:
/boot/config/shares/<name>.cfg (shareFloor=, persistent, read at array
start) and /var/local/emhttp/shares.ini (floor=, live — shfs
inotify-watches it, so no array restart was needed). Safe because emhttpd
generates shares.ini from the .cfg, so setting both to the same value
survives any regeneration. Backups: *.bak-2026-08-10-225809.
Unraid has no python3 — use PHP for scripted edits on the box.
Health + capacity status (2026-07-30)
- All disks are SMART-healthy — 0 reallocated sectors, 0 pending, 0 CRC errors — though two data drives are at ~54,500 power-on hours (~6.2 years).
- On 2026-08-10 disk1 + disk2 dropped inside the VM for ~1 h 45 m after a
vfio-pcicontroller reset. This was not drive failure: all six disks SMART-passed when they returned. Zero-parity Unraid reportedDISK_OK, so nothing alerted; theUnraidArrayDiskMissingPrometheus alert now covers it. - The array is deliberately zero-parity: the "2 disabled" slots shown in the UI are simply the empty parity slots, not failed drives.
- The real issue is capacity: disk4 96%, disk1 95%, disk6 93%, disk2 90%.
- 263.9 GB of byte-verified duplicates were deleted on 2026-07-28 —
duplicate movie downloads 146.5 G + audiobook duplicates 95.6 G + redundant
caabbackups 21.8 G (the GoPro and OneDrive trees were kept).
Array 91% full (analysed 2026-07-06)
/mnt/user: 24T used. The fill is almost entirely legitimate Media (22T) —
TV Shows 12T, Movies 9.6T — so there is no junk to delete; the real fix is the
NAS rebuild. The originally-suspected junk shares (DVDRips,
tdarrcache, lancache, fbshare) are already empty (0 bytes).
Short-term reclaim candidates (share-by-share du):
| Target | Size | Note |
|---|---|---|
downloads/ completed rips |
~300–400 G | 2160p imports already in Media (real copies, nlink=1) — but many are actively seeding in qBittorrent; prune per-file, not wholesale |
system/docker on the array |
316 G | two images (docker.img 100 G + docker1.img 250 G — likely one orphaned); ideally lives on cache SSD, not the array |
domains/ unused VM disks |
~170 G | MetaTrader4 + Macinabox Monterey — being converted to compressed qcow2 (2026-07-06) |
onedrive |
578 G | OneDrive sync copy |
Retired Unraid VMs archived offsite (2026-07-06) — so they're not forgotten
Proxmox is now the hypervisor of choice; new VMs go on the
pve1/pve2/pve3 cluster, not Unraid. The
two dormant Unraid VMs were compressed to qcow2 (MetaTrader4 166 G→55 G, Macinabox
Monterey 86 G→66 G), then archived encrypted to the Hetzner Storage Box at
hetzner-crypt:vm-archive/holly/ and removed from /mnt/user/domains. docker1.img
(250 G orphaned docker image) was also removed. To restore a VM: rclone copy
hetzner-crypt:vm-archive/holly/<name> <dest> (crypt creds in proxmox/backup/.env),
then attach the qcow2 to a new Proxmox VM.
Array Usage (2026-07-03)¶
| Disk | Size | Used | Avail | Use% |
|---|---|---|---|---|
| disk1 | 3.7T | 3.5T | 190G | 95% |
| disk2 | 3.7T | 3.5T | 236G | 94% |
| disk3 | 7.3T | 6.1T | 1.3T | 83% |
| disk4 | 3.7T | 3.5T | 184G | 96% |
| disk5 | 3.7T | 3.4T | 336G | 92% |
| disk6 | 3.7T | 3.4T | 262G | 93% |
Shares¶
/mnt/user: ClaireDrive, DVDRips, KiCad, MDHAdmin, Media, appdata, caab, domains, downloads, fbshare, games, lancache, onedrive, system, tdarrcache
Media Share Structure¶
Download Share Structure¶
Storage Best Practices¶
Cache Usage
- Prefer: Data written to cache first, then moved to array (good for appdata)
- Yes: Always on cache (fast but no parity protection)
- No: Direct to array (slower but parity protected)
- Only: Cache only, never moves to array
Parity Considerations
- Parity disk(s) must be equal to or larger than the largest data disk
- Two parity disks recommended for arrays with 6+ drives
- Monthly parity checks help detect drive issues early
- Holly currently runs zero parity by deliberate choice (see the status note above) — all six drives are data disks