---
title: "Homelab Media Stack — Jellyfin + *arr + qBittorrent Setup"
aliases: [media-stack, jellyfin-setup, arr-stack]
tags: [homelab, jellyfin, sonarr, radarr, prowlarr, qbittorrent, docker, gpu, quicksync]
created: 2026-04-26
updated: 2026-04-26
status: live
---

## Container Info

- **CT111** — 192.168.1.230, 4 GB RAM, Debian 12, unprivileged
- **GPU:** Intel HD Graphics 630, QuickSync via `/dev/dri/renderD129`
- **Media disk:** 500 GB LVM (`data-hdd:vm-111-media`) → `/mnt/media`
- **Docker data:** `/mnt/media/docker-data` (set via `data-root` in daemon.json)

---

## Directory Structure

All containers mount `/mnt/media` as `/data`. This is critical — without a unified mount, *arr apps copy instead of hardlink when moving downloads.

```
/mnt/media/
├── movies/                         # Radarr root folder: /data/movies
├── tv/                             # Sonarr root folder: /data/tv
└── downloads/
    ├── complete/
    │   ├── movies/                 # qBittorrent category "movies"
    │   └── tv/                     # qBittorrent category "tv"
    └── incomplete/                 # active torrents
```

**Ownership:** all dirs owned by `1000:1000` (linuxserver `abc` user = PUID/PGID=1000).

```bash
# Fix permissions if broken
ssh pve "pct exec 111 -- chown -R 1000:1000 /mnt/media/movies /mnt/media/tv /mnt/media/downloads"
```

---

## docker-compose.yml

Location: `/opt/media/docker-compose.yml` in CT111

```yaml
services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    restart: unless-stopped
    ports: ["8096:8096"]
    environment:
      - TZ=Europe/London
      - JELLYFIN_PublishedServerUrl=https://jellyfin.ai-impress.com
    volumes:
      - /opt/media/jellyfin/config:/config
      - /opt/media/jellyfin/cache:/cache
      - /mnt/media:/data
    devices:
      - /dev/dri/renderD129:/dev/dri/renderD129
      - /dev/dri/card1:/dev/dri/card1
    group_add: ["44", "993"]

  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    restart: unless-stopped
    ports: ["8989:8989"]
    environment: [PUID=1000, PGID=1000, TZ=Europe/London]
    volumes:
      - /opt/media/sonarr:/config
      - /mnt/media:/data

  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    restart: unless-stopped
    ports: ["7878:7878"]
    environment: [PUID=1000, PGID=1000, TZ=Europe/London]
    volumes:
      - /opt/media/radarr:/config
      - /mnt/media:/data

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    restart: unless-stopped
    ports: ["9696:9696"]
    environment: [PUID=1000, PGID=1000, TZ=Europe/London]
    volumes:
      - /opt/media/prowlarr:/config

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    restart: unless-stopped
    ports: ["8080:8080", "6881:6881", "6881:6881/udp"]
    environment: [PUID=1000, PGID=1000, TZ=Europe/London, WEBUI_PORT=8080]
    volumes:
      - /opt/media/qbittorrent:/config
      - /mnt/media:/data

networks:
  media:
    external: true
```

---

## Initial Setup Steps

### 1. qBittorrent — https://qbit.ai-impress.com

First login: temporary password from logs:
```bash
ssh pve "pct exec 111 -- docker logs qbittorrent 2>&1 | grep -i 'temporary password'"
```

**Tools → Options → Downloads:**
- Default save path: `/data/downloads/complete`
- Keep incomplete in: `/data/downloads/incomplete`

**Add categories** (right-click sidebar → Add Category):
| Category | Save Path |
|----------|-----------|
| `movies` | `/data/downloads/complete/movies` |
| `tv` | `/data/downloads/complete/tv` |

Change WebUI password immediately → save in Vaultwarden.

---

### 2. Radarr — https://radarr.ai-impress.com

**Settings → Media Management → Root Folders:**
```
/data/movies
```

**Settings → Download Clients → Add → qBittorrent:**
- Host: `qbittorrent`
- Port: `8080`
- Category: `movies`

---

### 3. Sonarr — https://sonarr.ai-impress.com

**Settings → Media Management → Root Folders:**
```
/data/tv
```

**Settings → Download Clients → Add → qBittorrent:**
- Host: `qbittorrent`
- Port: `8080`
- Category: `tv`

---

### 4. Prowlarr — https://prowlarr.ai-impress.com

**Settings → Apps → Add:**

| App | URL | API Key |
|-----|-----|---------|
| Sonarr | `http://sonarr:8989` | Settings → General → API Key |
| Radarr | `http://radarr:7878` | Settings → General → API Key |

After adding apps, Prowlarr syncs all indexers automatically.

---

### 5. Jellyfin — https://jellyfin.ai-impress.com

Run first-time setup wizard. Create admin account.

**Add Libraries:**
| Library | Path |
|---------|------|
| Movies | `/data/movies` |
| TV Shows | `/data/tv` |

**Enable QuickSync:**
Dashboard → Playback → Hardware acceleration:
- Acceleration type: **Intel QuickSync (VAAPI)**
- VA-API device: `/dev/dri/renderD129`
- Enable: H.264, HEVC, MPEG2, VC1, VP8, VP9, AV1 (all available)

Verify transcoding works: play a file → force transcode → Admin → Dashboard → Active Streams.

---

## GPU Passthrough Details (unprivileged LXC)

The Intel HD 630 iGPU is on `/dev/dri/card1` (not card0 — AMD display card is card0).

```ini
# /etc/pve/lxc/111.conf (relevant lines)
lxc.cgroup2.devices.allow: c 226:0 rwm
lxc.cgroup2.devices.allow: c 226:1 rwm
lxc.cgroup2.devices.allow: c 226:128 rwm
lxc.cgroup2.devices.allow: c 226:129 rwm
lxc.mount.entry: /dev/dri/card1 dev/dri/card1 none bind,optional,create=file
lxc.mount.entry: /dev/dri/renderD129 dev/dri/renderD129 none bind,optional,create=file
lxc.idmap: u 0 100000 65536
lxc.idmap: g 0 100000 44
lxc.idmap: g 44 44 1
lxc.idmap: g 45 100045 948
lxc.idmap: g 993 993 1
lxc.idmap: g 994 100994 64542
```

GID 44 = `video` group, GID 993 = `render` group — passed through unchanged via split idmap.

Proxmox host `/etc/subgid` must include:
```
root:100000:65536
root:44:1
root:993:1
```

Inside CT111, verify device access:
```bash
ssh pve "pct exec 111 -- ls -la /dev/dri/"
# renderD129 should be group render (GID 993)
# card1 should be group video (GID 44)
```

---

## Troubleshooting

**"Permission denied" on /dev/dri/renderD129 inside Jellyfin:**
```bash
# Check groups inside container
ssh pve "pct exec 111 -- docker exec jellyfin id"
# Should show gid 44 and 993 in groups
```

**Files not hardlinking (copy+delete instead of rename):**
- Check all containers use `/mnt/media:/data` mount (not split mounts)
- Source and destination must be same filesystem

**qBittorrent category not moving to correct folder:**
- Category save path must start with `/data/downloads/complete/<category>`
- Sonarr/Radarr import path is `/data/downloads/complete` (not per-category)
