---
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-05-03
status: live
---

## Container Info

- **CT111** — 192.168.1.230, 4 GB RAM, Debian 12, unprivileged
- **GPU:** Intel HD Graphics 630, QuickSync via `/dev/dri/renderD128` (not renderD128 — не существует)
- **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/
│   ├── films/                      # Radarr root 1: /data/movies/films → Jellyfin "Фильмы"
│   └── kids/                       # Radarr root 2: /data/movies/kids  → Jellyfin "Мультики"
├── tv/                             # Sonarr root folder: /data/tv
└── downloads/
    ├── tv-sonarr/                  # qBit category "tv-sonarr"
    ├── movies-radarr/              # qBit category "movies-radarr"
    └── manual/                     # qBit category "manual"
```

**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/renderD128:/dev/dri/renderD128
      - /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", "50000:50000", "50000:50000/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`

**Settings → Downloads → Default Save Path:** `/data/downloads/`

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

**Connection:** Listening port `50000` (fixed). UPnP: OFF. Router Virtual Server: 50000 TCP+UDP → 192.168.1.230:50000 ✅ done.

**WebUI whitelist:** `192.168.1.0/24,172.16.0.0/12` (Docker bridge для Dashbrr и др.)

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 | Note |
|---------|------|------|
| Фильмы | `/data/movies/films` | Radarr root 1 |
| Мультики | `/data/movies/kids` | Radarr root 2 |
| TV Shows | `/data/tv` | Sonarr root |

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

**Default language:**
- Dashboard → Playback → Default Audio Language: **Russian**
- Dashboard → Playback → Default Subtitle Language: **Russian**
- Subtitle mode: Smart (auto-load if audio not Russian)

**Webhooks (Sonarr + Radarr → Jellyfin):**
Settings → Connect → Add → Emby/Jellyfin:
- Host: `192.168.1.230`, Port: `8096`, API Key: `121facab6f7940c6a949501d144a6d3f`
- Events: On Import ✅, On Upgrade ✅, On Rename ✅, On Delete ✅

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

---

### 6. Bazarr — https://bazarr.ai-impress.com

Location: `/opt/media/bazarr/` (CT111, port 6767). Added 2026-05-03.

```yaml
services:
  bazarr:
    image: lscr.io/linuxserver/bazarr:latest
    environment: [PUID=1000, PGID=1000, TZ=Europe/London]
    volumes:
      - /opt/media/bazarr/config:/config
      - /mnt/media/movies:/movies
      - /mnt/media/tv:/tv
    ports: ["6767:6767"]
```

Settings → Languages → Single language: `Russian`, Fallback: `English`.
Settings → Providers → OpenSubtitles.com (requires account).
Settings → Sonarr: `http://sonarr:8989`, API key from Sonarr.
Settings → Radarr: `http://radarr:7878`, API key from Radarr.

---

### 7. Recyclarr — TRaSH-guides sync

Location: `/opt/media/recyclarr/` (CT111). Added 2026-05-03.

```yaml
services:
  recyclarr:
    image: ghcr.io/recyclarr/recyclarr:latest
    volumes:
      - ./config:/config
    environment: [TZ=Europe/London]
```

Config: `/opt/media/recyclarr/config/recyclarr.yml`. Run manually: `docker exec recyclarr recyclarr sync`.

---

## 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/renderD128 dev/dri/renderD128 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/"
# renderD128 should be group render (GID 993)
# card1 should be group video (GID 44)
```

---

## Troubleshooting

**"Permission denied" on /dev/dri/renderD128 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)
