Skip to content

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:

ModePoolDownload PathSAB PortTransmission Port
Full (docked)Mounted/var/mnt/pool/downloads80809091
Travel (portable)Not mounted/var/mnt/fast8tb/Local/downloads81809092

Current Issues:

  1. Two separate downloader instances with different ports
  2. Prowlarr/Sonarr/Radarr need reconfiguration when switching modes
  3. Main SABnzbd binds to home network IP (192.168.6.167) which doesn't exist on other networks
  4. 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.

An adversarial review identified fatal flaws with the symlink approach:

  1. Docker resolves symlinks at container start - Changing a symlink while containers run has NO effect. Containers are bound to the resolved target path.

  2. No hot-swap capability - You cannot dynamically switch storage backends for applications that maintain state about file locations.

  3. SELinux context issues - Symlinks receive different SELinux labels than their targets, causing permission errors.

  4. 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:

yaml
# 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:/downloads

2. Configure Prowlarr once (permanent)

Set download clients to portable instances:

  • SABnzbd: Host localhost, Port 8180
  • Transmission: Host localhost, Port 9092

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:

bash
#!/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"
fi

Benefits

  1. No configuration switching - Same ports, same paths, always
  2. No port conflicts - 8180/9092 never conflict with Steam (8080)
  3. Fast downloads - Always to NVMe, never to spinning rust
  4. Graceful degradation - Downloads work without pool
  5. Built-in queuing - *arr apps handle import deferral automatically
  6. No symlink complexity - Direct paths, no resolution issues
  7. 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

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

  1. ✅ Keep portable downloaders in docker-compose.reading.yml (already there)
  2. ⬜ Deprecate/remove main SABnzbd and Transmission from docker-compose.yml
  3. ⬜ Configure Prowlarr to use portable downloaders (one-time)
  4. ⬜ Update Sonarr/Radarr remote path mappings if needed
  5. ⬜ Test download → import flow in both modes

References

Built with ❤️ following Bell Labs standards. Dedicated to Stan Eisenstat.