Modernizing My Unraid Media Stack: From Monolithic Bloat to Decoupled Gluetun & qBittorrent

Modernizing My Unraid Media Stack: From Monolithic Bloat to Decoupled Gluetun & qBittorrent
Photo by Barney Goodman / Unsplash

How trimming legacy Docker containers, decoupling VPN networking, and automating torrent migration reclaimed gigabytes of storage and delivered a leak-proof setup-without losing a single byte of in-flight torrent data.


Introduction: The Ticking Debt of Monolithic Docker Images

Like many self-hosters and homelab enthusiasts, my Unraid server evolved organically over years of continuous uptime. When setting up automated media acquisition years ago, convenience was king. Monolithic "all-in-one" containers-specifically image stacks that bundled an entire Linux distribution, OpenVPN, iptables firewall scripts, and an application into a single container-were the easiest way to get up and running.

For a long time, containers like binhex-delugevpn served their purpose reliably. But homelab architecture moves fast, and over time, that convenience turned into technical debt:

  1. Extreme Image Bloat: Arch-based monolithic containers frequently exceeded 2.5 GB to 3.5 GB on disk, consuming scarce high-speed SSD cache storage.
  2. Coupled Lifecycles: When your VPN client, firewall, and torrent client are packaged together, you cannot update your torrent client without risking your VPN configuration, nor can you update your VPN routing without restarting all your active downloads.
  3. Outdated Torrenting Experience: Deluge, while a classic workhorse, has seen slower feature development compared to qBittorrent's rich feature set, responsive web UI, and rock-solid REST API.
  4. Dev Cache Leakage in Upstream Images: Ancillary containers like binhex-seerr had bloated to over 3.1 GB due to bundled headless browser binaries (Cypress/Chromium) left over from testing environments.

During a routine maintenance window, I decided it was time for a full-stack modernization:

  • Replace bloated companion containers with lean upstream images.
  • Decouple the VPN network layer completely using Gluetun.
  • Switch the torrent client to qBittorrent sharing Gluetun's network namespace.
  • Seamlessly migrate all active and seeding torrents with zero data loss.
  • Re-wire the *arr automation pipeline (Radarr & Sonarr).

Here is how the transition went, the architectural decisions behind it, the gotchas encountered, and the final results.


Phase 1: The Quick Win - Slimming Down Seerr (3.12 GB ? 503 MB)

Before overhauling the network stack, I audited the largest containers running on the Unraid cache pool. The immediate outlier was binhex-seerr at 3.12 GB.

Inspecting the container layers revealed that over 800 MB of that footprint came from Cypress Chromium cache directories bundled into the image build:

/root/.cache/Cypress/12.17.4/Cypress/Chromium.app  (~805 MB)

The official upstream image provided by the Seerr team (ghcr.io/seerr-team/seerr:latest) is a clean, multi-stage Alpine/Node build that weighs in at just 503 MB.

The Migration:

  1. Container Swapping: Pointed the Unraid template to ghcr.io/seerr-team/seerr:latest, mapping /mnt/user/appdata/binhex-seerr/seerr to the container's /app/config and running with --user 99:100 (nobody:users) to preserve file permissions.
  2. Validation: Seerr spun up in seconds, picked up the existing database instantly, and required zero reconfiguration.

Safety Backup: Created a timestamped snapshot of the SQLite database and settings directory:

cp -a /mnt/user/appdata/binhex-seerr/seerr /mnt/user/appdata/binhex-seerr/seerr_backup_$(date +%Y%m%d)

Result: An immediate 2.62 GB disk reduction from a 2-minute swap.


Phase 2: Decoupling the Network - Enter Gluetun

The core architectural change was breaking apart the VPN-torrent monolith. Rather than running a torrent client that manages its own VPN tunnel, modern best practice is to separate concerns:

  1. The VPN Router: A dedicated, security-audited container whose only job is managing tunnels, firewall kill-switches, and port forwarding.
  2. The Application: A lean torrent client with no VPN binaries or routing privileges of its own.
flowchart TD
    subgraph Host["Unraid Host (10.0.0.x)"]
        subgraph NetNamespace["Gluetun Network Namespace"]
            direction TB
            Gluetun["Gluetun Container<br/>(OpenVPN / Killswitch / Port Forward)"]
            qBit["qBittorrent Container<br/>(--net=container:gluetun)"]
        end
        Arr["Radarr / Sonarr<br/>(Connect to http://10.0.0.x:8085)"]
    end
    Internet((Internet))
    VPNServer["PIA VPN Gateway (Montreal)"]

    Gluetun -->|Encrypted VPN Tunnel| VPNServer
    qBit -->|All Outbound Traffic| Gluetun
    VPNServer --> Internet
    Arr -->|API & Download Control| qBit

Why Gluetun?

  • Written in Go: Lightweight, memory safe, and fast (<75 MB RAM footprint).
  • Leak-Proof Kill Switch: Built-in iptables rules ensure that if the VPN connection drops, outbound traffic is blocked immediately. No accidental IP leaks.
  • Native Port Forwarding: Gluetun includes built-in dynamic port forwarding handlers for providers like Private Internet Access (PIA), writing the assigned port directly to a local file (/tmp/gluetun/forwarded_port).
  • Container Namespace Sharing: Any downstream container can share Gluetun's network stack using standard Docker networking: --net=container:gluetun.

Deploying Gluetun:

Gluetun was deployed via Unraid's Docker engine with the following configuration:

docker run -d \
  --name=gluetun \
  --cap-add=NET_ADMIN \
  --device=/dev/net/tun \
  -v /mnt/user/appdata/gluetun:/gluetun \
  -v /tmp/gluetun:/tmp/gluetun \
  -e VPN_SERVICE_PROVIDER=custom \
  -e VPN_TYPE=openvpn \
  -e OPENVPN_CUSTOM_CONFIG=/gluetun/custom.ovpn \
  -e OPENVPN_USER="pXXXXXXX" \
  -e OPENVPN_PASSWORD="your_secure_password" \
  -e PORT_FORWARDING=on \
  -e PORT_FORWARDING_PROVIDER=privateinternetaccess \
  -e SERVER_NAME="montreal434" \
  -e FIREWALL_OUTBOUND_SUBNETS="10.0.0.0/24" \
  -p 8085:8085/tcp \
  --restart=unless-stopped \
  ghcr.io/qdm12/gluetun:latest
Key Networking Concept: Because downstream containers share Gluetun's network namespace, all host port mappings (such as qBittorrent's Web UI on port 8085) must be declared on the Gluetun container, not on the downstream container.

Phase 3: Deploying qBittorrent into the Tunnel

With Gluetun established and verifying an active public IP via the VPN gateway, linuxserver/qbittorrent was attached directly to Gluetun:

docker run -d \
  --name=qbittorrent \
  --net=container:gluetun \
  -e PUID=99 \
  -e PGID=100 \
  -e UMASK=022 \
  -e WEBUI_PORT=8085 \
  -v /mnt/user/appdata/qbittorrent:/config \
  -v /mnt/user/data/downloads:/downloads \
  --restart=unless-stopped \
  ghcr.io/linuxserver/qbittorrent:latest

Checking the Setup:

  1. Public IP Verification: Executing curl ifconfig.me inside the qBittorrent container returned the VPN exit IP (37.120.x.x), confirming that 100% of torrent traffic was routed through Montreal.
  2. Port Forwarding: Gluetun's PIA integration successfully requested a dynamic port from the gateway (23096). qBittorrent was configured to listen on this incoming port, ensuring optimal peer-to-peer connectivity.
  3. Web UI: Accessible via the host LAN IP at http://10.0.0.x:8085.

Phase 4: Zero-Data-Loss Torrent Migration

The biggest hesitation when replacing a torrent client is in-flight data. Nobody wants to re-download hundreds of gigabytes or lose active seeding ratios across private trackers.

Fortunately, Deluge stores all .torrent files in its state directory:

/mnt/user/appdata/binhex-delugevpn/deluge/state/*.torrent

Because both the old and new containers mapped to the same underlying persistent storage (/downloads/completed), a complete migration was achievable without moving a single data file:

  1. Extract Torrents: Copied all 51 active .torrent files into a staging directory.
  2. API Injection: Used qBittorrent's REST API (/api/v2/torrents/add) to batch-import every .torrent file with its download path pre-configured to /downloads/completed/ and paused state enabled.
  3. Force Recheck: Triggered a bulk recheck across all items.
# Automated bash pipeline for batch rechecking
COOKIE_JAR="/tmp/qbt_cookie.txt"
curl -s -i -c "$COOKIE_JAR" \
  -H "Referer: http://10.0.0.x:8085" \
  -d "username=admin&password=your_password" \
  http://10.0.0.x:8085/api/v2/auth/login >/dev/null

# Force recheck on all imported hashes
curl -s -b "$COOKIE_JAR" \
  -H "Referer: http://10.0.0.x:8085" \
  -d "hashes=all" \
  http://10.0.0.x:8085/api/v2/torrents/recheck

qBittorrent quickly verified the file chunks against the existing disk payload. Within minutes, the status confirmed:

  • Total Torrents: 51
  • Verified & Seeding: 51 (100% complete)
  • Corrupted or Re-downloaded: 0

Phase 5: Re-Wiring Radarr & Sonarr

With qBittorrent running and the torrents actively seeding, the last step was updating the automation pipeline.

Rather than manually clicking through multiple menus in Radarr and Sonarr, we used their REST APIs to swap the download client programmatically:

  1. Removed the old Deluge client ID.
  2. Added qBittorrent as the primary client:
    • Host: 10.0.0.x
    • Port: 8085
    • Credentials: Custom administrative account
    • Categories: radarr and tv-sonarr

Automated Connection Testing:
Executed an API-level test handshake:

POST /api/v3/downloadclient/test
HTTP/1.1 200 OK

Both Radarr and Sonarr immediately acknowledged the new client, mapped categories properly, and resumed automated queue management.


Gotchas & Lessons Learned

Every migration has subtle edge cases. Here are the three most important pitfalls encountered and how to resolve them:

1. WireGuard vs. OpenVPN with PIA

I initially planned to configure WireGuard within Gluetun. However, Private Internet Access does not provide static WireGuard private/public keys. Instead, PIA uses an ephemeral token handshake API that registers a dynamically generated public key with their gateway.

While Gluetun supports PIA WireGuard natively, custom endpoint tuning can cause handshake timeouts. Falling back to Gluetun's rock-solid OpenVPN client targeting PIA Montreal worked on the first attempt with zero latency penalties and full port forwarding.

2. qBittorrent API CSRF Protection

qBittorrent enforces strict Cross-Site Request Forgery (CSRF) protection on Web API endpoints like /api/v2/app/setPreferences. Direct curl calls returned 403 Forbidden until proper headers were passed:

-H "Referer: http://10.0.0.x:8085" \
-H "Origin: http://10.0.0.x:8085"

Always supply the correct origin headers when automating qBittorrent API calls.

3. Container Startup Order on Unraid

When using --net=container:gluetun, qBittorrent cannot start if Gluetun is not already up and running. If Docker boots both simultaneously upon system restart, qBittorrent will throw an error and exit.

The fix in Unraid is simple: configure autostart delays in /var/lib/docker/unraid-autostart:

  • gluetun: Delay 15 seconds (allows tunnel negotiation and DNS to stabilize)
  • qbittorrent: Delay 60 seconds

The Results: By the Numbers

Metric Before (Monolith Stack) After (Decoupled Stack) Improvement
Seerr Image Size 3.12 GB 503 MB -83.9% (-2.62 GB)
Deluge/qBit Image Size 2.50+ GB (Arch base) ~140 MB (Alpine base) -94.4% (-2.36 GB)
Total Docker Storage 9.33 GB 5.31 GB >4.0 GB Reclaimed
Combined RAM Usage ~450+ MB ~117 MB (Gluetun: 73MB, qBit: 44MB) ~74% Less Memory
VPN Security Coupled in app container Dedicated killswitch & network namespace Leak-Proof
Torrent Data Loss - 0 bytes 100% Preserved

Conclusion

Migrating away from legacy monolithic containers isn't just about saving a few gigabytes of disk space-it's about building a maintainable, resilient homelab.

By decoupling the VPN router from the torrent client, the system gains:

  • Independent updates for networking and applications.
  • Strict network namespace isolation with guaranteed kill-switch protection.
  • A modern, fast torrenting experience with active community support.
  • A much smaller footprint on our SSD cache pool.

If you are still running monolithic VPN-application containers, making the leap to Gluetun is well worth the afternoon investment.

Fahim

Fahim

Engineer, husband, dad, and photography enthusiast documenting technology, creativity, and the occasional rabbit hole.
Ottawa, ON