Jouskaio.me

The value of an idea lies in the using of it

Auditing and Consolidating a Self-Hosted Media Stack: Hardlinks, Storage Cleanup, and Safe Migration

Auditing and Consolidating a Self-Hosted Media Stack

After running a home media server in production for several weeks, storage fragmentation and orphaned files inevitably accumulate. This article documents a comprehensive September 2026 audit to reclaim space, validate architecture integrity, and establish a repeatable maintenance process — with emphasis on hardlink preservation and safe cleanup strategies.

The Initial Problem: Fragmented Mount Paths

The stack initially worked, but the storage layout was not path-clean. Instead of a single unified mount, media was spread across three separate NFS mount points:

  • /mnt/movies — Radarr import destination
  • /mnt/tv — Sonarr import destination
  • /mnt/downloads — qBittorrent download source

This fragmentation prevented hardlinking between the download directory and the library directories. When Radarr imported a film from /mnt/downloads to /mnt/movies, it had to use cp() instead of link(), doubling disk usage for every single import. Over time, the impact was massive: a 60 GB film imported to Radarr would consume 120 GB total (60 GB in downloads, 60 GB in movies).

The Solution: Unified /data Mount Architecture

The refactored architecture consolidates all media under a single NFS mount exposed consistently to all containers:

On the Host

mount -t nfs 192.168.1.19:/volume1/Media /mnt/media

In Docker Compose

Every download and media-management service uses the same bind mount:

services:
  qbittorrent:
    volumes:
      - /mnt/media:/data
  radarr:
    volumes:
      - /mnt/media:/data
  sonarr:
    volumes:
      - /mnt/media:/data

Inside the Containers

All services see a single unified layout:

/data/downloads  → qBittorrent save path
/data/movies     → Radarr root folder
/data/series     → Sonarr root folder

Why This Matters

Since all three directories are on the same NFS filesystem (same inode table), hardlinks work seamlessly across services. When qBittorrent downloads a 60 GB film to /data/downloads and Radarr imports it to /data/movies, Radarr uses link() instead of cp(). The result: one physical copy, two logical paths, zero additional disk usage.

Audit Results: September 2026

Component Initial State After Audit Outcome
Radarr orphaned films ~176 leftover downloads ~115 properly imported ~150 GiB reclaimed
Sonarr episode orphans Unknown 0 true orphans found Series library confirmed clean
qBittorrent Sonarr torrents 96 total entries 59 active torrents 37 inactive torrents removed
Empty directories 11 orphaned 0 remaining Filesystem cleaned
Hardlink verification Unconfirmed 5 active hardlinks verified Hardlink architecture validated

Audit Methodology

1. Radarr Orphan Detection

Cross-reference Radarr’s database of imported films with the actual files in /mnt/media/downloads. Mark a file as a safe orphan candidate if:

  • It is NOT referenced by any active Radarr entry
  • It has links=1 (no hardlink protection)
  • It is not associated with any active qBittorrent torrent

Result: ~61 films safely deleted, freeing approximately 150 GiB.

2. Sonarr Episode Audit

Similarly audit Sonarr series, checking for episodes that:

  • Do not belong to any active Sonarr series entry
  • Are not associated with active qBittorrent torrents
  • Have links=1 (not hardlink-protected)

Result: 0 orphans found. The Sonarr library was completely clean — all existing episodes were accounted for and properly imported.

3. qBittorrent Inactive Torrent Cleanup

Identify torrents that are safe to remove from qBittorrent without reclaiming significant disk space. Filter by:

  • category=sonarr (only Sonarr-managed entries)
  • progress=0.0 (never started downloading)
  • State in {queuedDL, metaDL} (stuck in queue, not downloading)

These torrents were added by Sonarr but never actually began their download. Removing them did not reclaim significant space, but it reduced database clutter and made qBittorrent easier to reason about.

Result: 37 inactive torrents removed. qBittorrent database reduced from 96 to 59 entries.

For any imported content, verify that hardlinks are actually in place and protecting the data:

find /mnt/media/series -type f -exec stat -c '%h %n' {} \; | awk '$1 > 1'

This command finds all files in the series directory with a link count greater than 1.

Result: 5 files with links=2 confirmed in /mnt/media/series/Chernobyl/Season 01. These represent hardlinked copies between the download directory and the series import directory. One physical file, two logical paths.

Key Lessons Learned

Container Path Mapping Is Critical

When auditing, never mix container-internal paths with host paths without first validating the mapping. In this case, qBittorrent reported content under /data/downloads, but the host filesystem showed /mnt/media/downloads. A naive audit script could incorrectly label active torrent directories as orphans if it did not account for this mapping.

Always validate the bind mount before trusting any cleanup decision:

docker inspect qbittorrent --format '{{range .Mounts}}{{println .Source " -> " .Destination}}{{end}}'

In this case, the critical mapping was: /mnt/media (host) → /data (container).

Torrent State vs. Physical Content

A torrent in queuedUP state with progress=1.0 is actively seeding completed content that is already hardlinked into your library. Do NOT delete the torrent simply because it is not actively downloading. Removing it stops peer contribution but reclaims zero disk space (the hardlinked files remain in the library). The content is protected by being referenced from both the downloads directory and the library directory.

Seeing the same episode in both a download directory and a library directory does not automatically mean the file is duplicated. Check the link count first. If links > 1, the file is hardlinked and deleting either copy while the other exists will reduce the link count but not reclaim disk space until the last link is removed.

Safe Cleanup Requires Conservative Assumptions

When in doubt, keep the file. The cost of keeping extra data is measured in storage; the cost of deleting something irreplaceable is immeasurable. Always cross-reference with at least two sources (Radarr/Sonarr database, qBittorrent content mapping, filesystem link count) before marking a file for deletion.

  • Monthly: Review Radarr leftovers in /mnt/media/downloads. Films accumulate quickly as new releases are added.
  • Quarterly: Verify Sonarr hardlinks and active torrent mappings. Ensure the Docker network and storage paths are still consistent.
  • After any container recreation: Validate that the /mnt/media:/data bind mount is still in place and that all services still see a unified filesystem.
  • Before any major storage operation: Audit empty directories, queued torrents, and save-path consistency to avoid unexpected space issues.

Security Considerations

If you expose the stack to the internet, keep the public surface minimal. Only Jellyfin and Jellyseerr should be externally accessible. qBittorrent, Radarr, Sonarr, and Prowlarr must stay behind local-only access controls, a VPN, or strict reverse-proxy middleware. Exposing these management interfaces publicly creates significant security risks.

Why This Audit Mattered

Running a media stack on a fixed-size storage device requires discipline. Without a repeatable audit process, storage gradually fragments, old downloads accumulate invisibly, and cleanup becomes a risky guessing game. By establishing clear criteria for what is safe to delete, validating hardlink protection, and verifying Docker path mappings, the audit process became trustworthy enough to automate.

The consolidated /data layout is not just cleaner — it makes audits dramatically simpler. All media lives under one filesystem, all services see consistent paths, and hardlink validation is straightforward. Future audits will be faster and safer.


Stack: Jellyfin, Jellyseerr, Radarr, Sonarr, Prowlarr, qBittorrent, Bazarr. Infrastructure: Proxmox LXC, Synology NAS over NFS, Docker Compose. Audit conducted September 2026.

Newsletter

Stay in the loop! ✨
 
Subscribe to my newsletter to get my latest updates, new articles, and thoughts on tech, projects, and what I’m building.

Leave a Reply

Your email address will not be published. Required fields are marked *

×