Skip to content

Arr Stack LXC

An unprivileged LXC container (ID 111, hostname docker-media) running a Docker Compose–based media automation stack. The stack covers the full download-to-library pipeline: Prowlarr (indexer management) → Radarr / Sonarr (movie / TV automation) → qBittorrent (downloading) → Jellyseerr (request frontend).

Related: Jellyfin LXC

Radarr and Sonarr write completed media to /mnt/data, which is also the media root for the Jellyfin LXC. Make sure both containers mount the same underlying storage path via Proxmox bind mounts.


System specs

Setting Value
LXC ID 111
Hostname docker-media
Privilege level Unprivileged
OS Debian GNU/Linux 13 (Trixie)
Kernel Linux 6.17.13-4-pve
CPU 2 cores
RAM 2048 MB
Root disk 20 GB

Services & ports

Service Image Port
qBittorrent lscr.io/linuxserver/qbittorrent:latest 8080 (WebUI), 6881 (peers)
Prowlarr lscr.io/linuxserver/prowlarr:latest 9696
Radarr lscr.io/linuxserver/radarr:latest 7878
Sonarr lscr.io/linuxserver/sonarr:latest 8989
Jellyseerr fallenbagel/jellyseerr:latest 5055

Part 1 — LXC provisioning

Step 1: Run the community helper script

Run this command directly in the Proxmox node shell:

bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/tools/arr-stack.sh)"

Step 2: Advanced installation choices

Select Advanced mode when prompted. The following options were used:

Prompt Choice Reason
Deployment mode Advanced Required to customise storage, network, and resources
OS distribution Debian (version 13 / Trixie)
Container type UnprivilegedYes Full root isolation from Proxmox host
Root credentials Custom password Supplied own secure root password
CPU 2
RAM 2048
Disk size 20G For app configs and databases
Network type DHCP Temporary; locked permanently via router DHCP reservation on the container's MAC
IPv6 none / disable Mitigates ambient VPN leaks and DNS routing bypasses
Interface MTU 1500 Default standard Ethernet; guarantees packet stability
DNS search domain Blank Inherits from Proxmox host, routes through the network Pi-hole
VLAN tag Blank Pins container to primary LAN plane (vmbr0)
Nesting Enabled Mandatory for Docker inside LXC
FUSE support Disabled Unnecessary; local storage bypasses userspace mounting
TUN/TAP device support Enabled Required if using gluetun for WireGuard/OpenVPN tunnels
Filesystem mounts Blank Cross-linking handled via Proxmox host bind mounts (see Part 2)
Portainer UI Disabled Management via Docker Compose; no extra footprint needed
Docker TCP socket Disabled Docker communicates over the local UNIX socket only

Part 2 — Storage bind mount

Expose your media drives to the container from the Proxmox host shell. Replace /your/host/storage/path with your real folder location:

pct set 111 -mp0 /your/host/storage/path,mp=/mnt/data

This makes /mnt/data inside the LXC point at the host path. Radarr and Sonarr both mount /mnt/data so they can read torrents and write to the media library from the same volume.


Part 3 — Security hardening

Step 1: Create a sudo user (run in the Proxmox LXC console)

Log into the Proxmox Web UI, open the Console for LXC 111 (auto-logs in as root), and run:

# Create your new user and set a password
adduser yourusername

# Add to sudo group
usermod -aG sudo yourusername

# Temporarily allow password authentication for SSH
nano /etc/ssh/sshd_config
# Verify or set: PasswordAuthentication yes

# Apply the change
systemctl restart sshd

Step 2: Push your SSH public key (run from your laptop)

Replace 192.168.1.X with the current DHCP IP of LXC 111:

ssh-copy-id yourusername@192.168.1.X

If ssh-copy-id is unavailable:

ssh yourusername@192.168.1.X "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys" < ~/.ssh/id_rsa.pub

Step 3: Test key-based authentication (run from your laptop)

Open a new terminal window and confirm you can log in without a password prompt:

ssh yourusername@192.168.1.X

Step 4: Disable password auth and root SSH (run inside the LXC via your new SSH session)

sudo nano /etc/ssh/sshd_config

# Set these exact lines:
# PermitRootLogin no
# PasswordAuthentication no

sudo systemctl restart sshd

Part 4 — Docker Compose stack

Current setup: no VPN

The stack currently runs qBittorrent with direct port exposure — no VPN tunnel. The gluetun service and the VPN-routed qBittorrent block are preserved in the compose file as comments so the VPN migration (Part 5) is a straightforward uncomment-and-swap.

docker-compose.yml
services:
  # gluetun:
  #   image: qmcgaw/gluetun
  #   container_name: gluetun
  #   cap_add:
  #     - NET_ADMIN
  #   devices:
  #     - /dev/net/tun:/dev/net/tun
  #   volumes:
  #     - ./gluetun:/gluetun
  #   environment:
  #     - VPN_SERVICE_PROVIDER=your_vpn_provider # e.g., mullvad, protonvpn, surfshark
  #     - VPN_TYPE=wireguard # Or openvpn
  #     - WIREGUARD_PRIVATE_KEY=your_private_key
  #     - WIREGUARD_ADDRESSES=your_vpn_ip
  #     # - TZ=America/Phoenix
  #   ports:
  #     - 8080:8080 # qBittorrent WebUI (routed through gluetun)
  #   restart: always

  # qbittorrent:
  #   image: lscr.io/linuxserver/qbittorrent:latest
  #   container_name: qbittorrent
  #   network_mode: "container:gluetun" # Forces traffic through VPN
  #   environment:
  #     - PUID=1000
  #     - PGID=1000
  #     - TZ=America/Phoenix
  #     - WEBUI_PORT=8080
  #   volumes:
  #     - ./qbittorrent-config:/config
  #     - /mnt/data/torrents:/mnt/data/torrents
  #   restart: always

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    # network_mode: "container:gluetun"  <-- Comment this line out
    ports:
      - 8080:8080 # Exposes the WebUI directly to your LXC network
      - 6881:6881 # Standard incoming torrent peer traffic port
      - 6881:6881/udp
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=America/Phoenix
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent-config:/config
      - /mnt/data/torrents:/mnt/data/torrents
    restart: always

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=America/Phoenix
    volumes:
      - ./prowlarr-config:/config
    ports:
      - 9696:9696
    restart: always

  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=America/Phoenix
    volumes:
      - ./radarr-config:/config
      - /mnt/data:/mnt/data # Needs access to both torrents and media
    ports:
      - 7878:7878
    restart: always

  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=America/Phoenix
    volumes:
      - ./sonarr-config:/config
      - /mnt/data:/mnt/data # Needs access to both torrents and media
    ports:
      - 8989:8989
    restart: always

  jellyseerr:
    image: fallenbagel/jellyseerr:latest
    container_name: jellyseerr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=America/Phoenix
    volumes:
      - ./jellyseerr-config:/app/config
    ports:
      - 5055:5055
    restart: always

To bring the stack up, place docker-compose.yml in a working directory on the LXC (e.g. /home/yourusername/arr-stack/) and run:

cd /home/yourusername/arr-stack
sudo docker compose up -d

Volume layout

Each service stores its config in a directory next to the compose file:

arr-stack/
├── docker-compose.yml
├── qbittorrent-config/
├── prowlarr-config/
├── radarr-config/
├── sonarr-config/
└── jellyseerr-config/

Radarr and Sonarr also mount /mnt/data (the host bind mount from Part 2) for access to both the torrent download directory and the media library.


Part 5 — Adding a VPN (future)

When a VPN subscription is in place, route qBittorrent traffic through gluetun so all torrent traffic leaves via the VPN tunnel. The compose file already contains the commented-out blocks for both services — the migration is:

Step 1: Fill in gluetun credentials

In docker-compose.yml, uncomment the gluetun service and populate:

- VPN_SERVICE_PROVIDER=your_vpn_provider  # e.g. mullvad, protonvpn, surfshark
- VPN_TYPE=wireguard                       # or openvpn
- WIREGUARD_PRIVATE_KEY=your_private_key
- WIREGUARD_ADDRESSES=your_vpn_ip

Step 2: Switch qBittorrent to VPN-routed mode

Comment out the current qbittorrent service block (the one with direct ports: exposure) and uncomment the VPN-routed block directly below the gluetun service. The key difference is:

# VPN-routed qBittorrent — no ports: block of its own
network_mode: "container:gluetun"  # all traffic exits through the gluetun container

The WebUI port (8080) is still reachable on the LXC's IP because gluetun publishes it in its own ports: block.

Step 3: Ensure TUN/TAP is enabled on the LXC

TUN/TAP was enabled at provisioning time (see Part 1, Step 2). If the container was originally created without it, enable it from the Proxmox host shell:

pct set 111 -features tun=1

Then restart the container.

Step 4: Bring the stack back up

sudo docker compose down
sudo docker compose up -d

Confirm gluetun connects before qBittorrent starts downloading — gluetun logs will show the VPN tunnel status.

IPv6 leak risk

IPv6 was disabled at the LXC level during provisioning (see Part 1). If you re-enable it later, ensure your VPN provider's gluetun config also disables IPv6 or your torrent traffic may leak outside the tunnel.