ADR: Unified Downloader Architecture for Travel/Full Mode
Status: Accepted (Revised) Date: 2026-01-09 Revised: 2026-01-09 after adversarial review Context: SAB/Transmission need to work in both travel mode (no pool) and full mode (pool mounted)
Problem Statement
The media stack has two operating modes:
| Mode | Pool | Download Path | SAB Port | Transmission Port |
|---|---|---|---|---|
| Full (docked) | Mounted | /var/mnt/pool/downloads | 8080 | 9091 |
| Travel (portable) | Not mounted | /var/mnt/fast8tb/Local/downloads | 8180 | 9092 |
Current Issues:
- Two separate downloader instances with different ports
- Prowlarr/Sonarr/Radarr need reconfiguration when switching modes
- Main SABnzbd binds to home network IP (192.168.6.167) which doesn't exist on other networks
- Steam uses localhost:8080, conflicting with SABnzbd
Decision
Use a symlink-based download path abstraction REJECTED - See "Why Symlinks Don't Work" below.
Use single downloader stack with local-first downloads and deferred imports.
Why Symlinks Don't Work
An adversarial review identified fatal flaws with the symlink approach:
Docker resolves symlinks at container start - Changing a symlink while containers run has NO effect. Containers are bound to the resolved target path.
No hot-swap capability - You cannot dynamically switch storage backends for applications that maintain state about file locations.
SELinux context issues - Symlinks receive different SELinux labels than their targets, causing permission errors.
Incomplete downloads path mismatch - SABnzbd's incomplete directory would still point to local even if complete directory pointed to pool.
Correct Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ UNIFIED DOWNLOADER ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ALWAYS USE PORTABLE DOWNLOADERS (both modes) │
│ ├── SABnzbd: localhost:8180 (avoids Steam conflict on 8080) │
│ └── Transmission: localhost:9092 │
│ │
│ DOWNLOADS ALWAYS GO TO LOCAL FAST STORAGE │
│ └── /var/mnt/fast8tb/Local/downloads │
│ ├── complete/ ← Finished downloads │
│ ├── incomplete/ ← In-progress downloads │
│ └── watch/ ← Torrent watch folder │
│ │
│ IMPORTS DEFERRED UNTIL POOL AVAILABLE │
│ └── *arr apps import from local → pool media libraries │
│ └── If pool unavailable, imports queue (built-in *arr behavior) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘Key Insight
Downloads and media libraries operate on different storage timelines.
- Downloads need fast storage (NVMe/SSD) for speed
- Media libraries need large storage (pool) for capacity
- *arr apps already have "pending imports" feature - leverage it!
Implementation
1. Use ONLY portable downloaders
Remove or disable main SABnzbd (8080) and Transmission (9091) from docker-compose.yml. The reading stack's portable downloaders are the ONLY download clients:
# docker-compose.reading.yml - THESE ARE THE ONLY DOWNLOADERS
sabnzbd-portable:
ports: ["8180:8080"]
volumes:
- /var/mnt/fast8tb/Local/downloads:/downloads
transmission-portable:
ports: ["9092:9091"]
volumes:
- /var/mnt/fast8tb/Local/downloads:/downloads2. Configure Prowlarr once (permanent)
Set download clients to portable instances:
- SABnzbd: Host
localhost, Port8180 - Transmission: Host
localhost, Port9092
These settings never change regardless of mode.
3. Configure *arr apps download paths
Sonarr/Radarr/etc. need to know:
- Download path:
/var/mnt/fast8tb/Local/downloads(container sees/downloads) - Media library:
/var/mnt/pool/media/...(only available in full mode)
When pool is unavailable:
- Downloads complete successfully (to local)
- Imports queue until pool mounts
- No configuration changes needed
4. Optional: Migration script for travel downloads
When returning home with downloads from travel:
#!/bin/bash
# migrate-downloads-to-pool.sh
# Run after docking to move completed downloads to pool
LOCAL="/var/mnt/fast8tb/Local/downloads/complete"
POOL="/var/mnt/pool/downloads/complete"
if mountpoint -q /var/mnt/pool; then
rsync -av --remove-source-files "$LOCAL/" "$POOL/"
echo "Migrated downloads to pool"
else
echo "Pool not mounted - skipping migration"
fiBenefits
- No configuration switching - Same ports, same paths, always
- No port conflicts - 8180/9092 never conflict with Steam (8080)
- Fast downloads - Always to NVMe, never to spinning rust
- Graceful degradation - Downloads work without pool
- Built-in queuing - *arr apps handle import deferral automatically
- No symlink complexity - Direct paths, no resolution issues
- SELinux safe - No symlink context propagation issues
Trade-offs
- Downloads always use local storage first (actually a benefit for speed)
- May need to run migration script after returning from travel
- Local storage must have space for downloads (8TB internal is plenty)
Rejected Alternatives
A. Symlink-based path switching
REJECTED - Docker resolves symlinks at container start, not dynamically. Cannot hot-swap storage.
B. Environment variable switching (.env.local / .env.pool)
REJECTED - Still requires container restart. More complex than single config.
C. Keep dual-instance approach
REJECTED - Requires Prowlarr reconfiguration when switching modes.
D. HAProxy/nginx proxy
REJECTED - Massive overengineering for this use case.
Migration Path
- ✅ Keep portable downloaders in docker-compose.reading.yml (already there)
- ⬜ Deprecate/remove main SABnzbd and Transmission from docker-compose.yml
- ⬜ Configure Prowlarr to use portable downloaders (one-time)
- ⬜ Update Sonarr/Radarr remote path mappings if needed
- ⬜ Test download → import flow in both modes
References
- Pool Health Architecture
- smart-start.sh
- Adversarial review: 2026-01-09 deep-thinker analysis