A torrent that is seeding and a file that is in the library should be the same file. The moment they live on two filesystems, they can’t be — and everything downstream of that mistake costs double the disk and ends in a full-quota outage.This page documents a portable storage layout for a download-automation media stack (a torrent client, the
*arr managers, and a media server). It is the TRaSH-guides single-filesystem layout, realized on ZFS.
The two-dataset anti-pattern
The intuitive layout is two datasets: one for downloads, one for the library. Each gets its own quota, each is bind-mounted where its service expects it. It looks tidy and it fails in three compounding stages:EXDEV— hardlinks are impossible. A hardlink can only exist within one filesystem. Two ZFS datasets are two filesystems, so when the*arrimport step tries to hardlink a completed download into the library,link()fails withEXDEVand the importer silently falls back to copy.- Every seeded item is duplicated. The copy means each item now exists twice — once in the download dataset (still seeding) and once in the library. Disk usage for anything you keep seeding is exactly 2×.
- The quota death spiral. The download dataset’s quota fills with seed copies that never leave. When it hits 100%, the torrent client can no longer write — active downloads fail, the client errors out or crashes, and the whole ingest pipeline stops until a human intervenes.
The fix: one dataset, one mount, two subtrees
Replace both datasets with one ZFS dataset, bind-mounted into every guest at the same path —/data — with the download and library trees as plain subdirectories:
- Import = hardlink.
torrents/andmedia/are the same filesystem, so the*arrimport is an instant, atomiclink()— no copy window, no partial files. - Seeding is the library. The seeded file and the library file are one inode. Keeping a torrent seeding costs zero extra bytes, forever.
- Atomic moves. Renames and upgrades within
/dataare metadata operations, never data rewrites. - One quota. A single quota on the dataset bounds the whole media footprint. There is no second quota to mis-split, and no way for the download tree to starve while the library tree has headroom.
recordsize=1M. Media files are large and sequential; the 1M recordsize cuts metadata overhead and suits both torrent IO and streaming reads.
Shared group + setgid
Every service in the stack must agree on ownership, or imports fail on permissions instead ofEXDEV. The portable pattern:
- One shared group (e.g.
media) containing the torrent client, each*arr, and the media server user. chgrp -R media /dataandchmod 2775(setgid) on the directories, so everything created under/datainherits the group automatically.
Resilience settings
The layout removes the structural failure; these settings handle the operational ones. Each guards a different link in the chain:| Setting | Where | What it prevents |
|---|---|---|
Restart=on-failure | torrent client’s systemd unit | a client crash silently stopping ingest until someone notices |
| Seed-ratio limit (e.g. 5×) then pause | torrent client | unbounded seeding obligations; 5× is generous stewardship while still letting torrents finish their lifecycle |
minimumFreeSpaceWhenImporting | each *arr | imports writing the disk to 100% — the *arr stops importing before the filesystem is full |
| Dataset capacity alert at >85% | monitoring | discovering a full pool only when writes start failing |
| Start-on-boot | every guest in the stack | a host reboot leaving one link of the pipeline down |
What this connects to
Media stack
The services that sit on top of this layout.
ZFS pool naming
The tier-named pool the dataset lives in.
ZFS backup & replication
Where re-downloadable media sits in the snapshot/replication policy.
LXC vs Docker
Why the stack runs as LXCs with bind mounts in the first place.