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 | Unprivileged → Yes |
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.