Medienserver mit Jellyfin und Automatic Ripping Machine in LXC-Containern unter Proxmox
Ich wollte meine DVD-Sammlung digitalisieren und dauerhaft über einen eigenen Medienserver verfügbar machen. Daraus ist ein kleines, zusammenhängendes Projekt auf meinen bestehenden Proxmox-Hosts (Intel N100) geworden: Jellyfin für die Bibliothek und das Streaming, die Automatic Ripping Machine (ARM) für das automatisierte Rippen eingelegter DVDs, sowie ein kleiner Fileserver-Container, über den ich Dateien manuell in die Bibliothek einspielen kann. Alle drei Dienste laufen in eigenen LXC-Containern auf demselben Host statt in vollständigen VMs - das spart Ressourcen, bringt aber ein paar Besonderheiten mit sich, insbesondere beim Durchreichen der Intel-iGPU und des optischen Laufwerks. Genau diese Stolpersteine dokumentiere ich hier.
Kapitel 1: Architektur im Überblick
Die Grundidee: Ein Proxmox-Host mit Intel-N100-CPU (Alder Lake-N) bringt bereits eine Quick-Sync-fähige iGPU
mit, die sich für Jellyfins Transcoding-Bedarf nutzen lässt. Statt dafür eine dedizierte VM mit vollem
GPU-Passthrough einzurichten - was die iGPU exklusiv an eine einzige VM binden würde, da Alder Lake-N kein
GVT-g/SR-IOV unterstützt, teile ich mir die iGPU über mehrere LXC-Container. Da LXC den Kernel
des Hosts nutzt, reicht ein einfaches Device-Passthrough von /dev/dri/renderD128, das mehrere
Container gleichzeitig nutzen können.
Ich habe drei Container angelegt:
- Jellyfin - die eigentliche Medienbibliothek und der Streaming-Server
- Fileserver - ein schlanker Container mit Samba, über den ich Dateien manuell in die Bibliothek kopieren kann
- ARM - rippt automatisch eingelegte DVDs und legt die fertigen Dateien direkt in der Bibliothek ab
Alle drei greifen auf denselben Datenbestand zu: ein ZFS-Dataset auf dem Proxmox-Host, das ich per Bind
Mount (nicht als separates, containergebundenes Volume) in jeden der drei Container unter
/mnt/data einbinde. Damit landen gerippte Filme, manuell kopierte Dateien und das, was Jellyfin
letztlich anzeigt, immer am selben Ort - ohne Umweg über eine Netzwerkfreigabe zwischen den Containern.
An den Host ist ein externes DVD-Laufwerk per USB angeschlossen, das an den ARM-Container durchgereicht wird.
Kapitel 2: Intel-Treiber auf dem Proxmox-Host vorbereiten
Bevor die iGPU in irgendeinen Container durchgereicht werden kann, muss der Host sie selbst ansteuern können. Zunächst prüfen, ob der Kernel die iGPU überhaupt erkennt:
ls -la /dev/dri/
Hier sollten card0 und renderD128 auftauchen. Danach die passenden Treiberpakete
installieren:
apt update
apt install -y intel-media-va-driver-non-free vainfo intel-gpu-tools
Wichtig: Das Paket intel-media-va-driver-non-free liegt in der non-free-Komponente von
Debian, nicht in non-free-firmware (die beiden werden seit Debian 12 getrennt
geführt). Falls apt den Installationskandidaten nicht findet, in /etc/apt/sources.list prüfen, ob
beide Komponenten enthalten sind:
deb http://deb.debian.org/debian trixie main contrib non-free non-free-firmware
Nur mit dem freien Paket intel-media-va-driver lässt sich zwar decodieren, aber nicht encodieren -
und genau der Encode-Schritt ist es, den Jellyfin beim Transcoding braucht. Nach der Installation lässt sich die
Funktion prüfen:
vainfo
Eine Liste unterstützter VAProfile-Einträge bestätigt, dass der Treiber korrekt geladen ist.
Kapitel 3: Der Jellyfin-Container
Container anlegen
Für Jellyfin genügt ein schlanker Container:
- Debian als Template
- Privilegiert - vereinfacht das Device-Passthrough der iGPU deutlich
- 2-4 CPU-Kerne, 2-4 GB RAM (die eigentliche Transcoding-Arbeit läuft über die iGPU, nicht über CPU-Kerne)
- Kleines rootfs (8-16 GB reichen), da die Mediendaten separat über den Bind Mount kommen
iGPU durchreichen
Über die Proxmox-GUI: Container auswählen → Ressourcen → Hinzufügen → Passthrough-Gerät →
/dev/dri/renderD128. Alternativ in /etc/pve/lxc/<CTID>.conf:
dev0: /dev/dri/renderD128,mode=0666
Der mode=0666-Zusatz war bei mir notwendig, weil das Gerät im Container standardmäßig root:root
gehört und der spätere Jellyfin-Dienstnutzer sonst keinen Zugriff hätte, unabhängig von Gruppenmitgliedschaften.
Jellyfin installieren
Ich installiere Jellyfin nativ, ohne Docker, über das offizielle Installationsskript:
curl -fsSL https://repo.jellyfin.org/install-debuntu.sh | sudo bash
sudo systemctl enable --now jellyfin
Das Skript installiert neben Jellyfin auch jellyfin-ffmpeg, eine für Jellyfin gepatchte
FFmpeg-Version, die für Quick-Sync-Transcoding benötigt wird - die Standard-FFmpeg-Pakete der Distribution
reichen dafür nicht.
Damit der jellyfin-Systemnutzer auf die iGPU zugreifen darf:
sudo usermod -aG render,video jellyfin
sudo systemctl restart jellyfin
Mediendaten einbinden und Bibliothek einrichten
Den ZFS-Datenbestand vom Proxmox-Host als Bind Mount einbinden (in /etc/pve/lxc/<CTID>.conf):
mp1: /rpool/data/subvol-100-disk-1,mp=/mnt/data
Danach in der Jellyfin-Weboberfläche (http://<Container-IP>:8096) den Einrichtungsassistenten
durchlaufen und die Bibliothek(en) unter /mnt/data anlegen.
Hardwarebeschleunigung aktivieren
Unter Dashboard → Wiedergabe → Transkodierung:
- Hardwarebeschleunigungsmethode: Intel QuickSync (QSV)
- Gerät:
/dev/dri/renderD128 - H.264 und HEVC jeweils für Hardware-Decodierung und -Encodierung aktivieren
Ob es funktioniert, lässt sich beim Abspielen mit erzwungener Qualitätsreduzierung (Zahnrad-Symbol im Player → Qualität → niedrigere Bitrate wählen) und einem Blick auf den Host prüfen:
intel_gpu_top
Steigt der Wert bei "Video" während der Wiedergabe an, transkodiert tatsächlich die iGPU statt der CPU. Bei mir kam es zunächst zu einem "Wiedergabefehler", weil die nötigen Treiber - anders als auf dem Proxmox-Host - im Container selbst noch fehlten. Kapitel 2 (Treiberinstallation) muss also zusätzlich im Jellyfin-Container wiederholt werden, da Docker/LXC zwar den Kernel, nicht aber die Userspace-Bibliotheken mit dem Host teilen.
Kapitel 4: Ein eigener Fileserver-Container für den Datenzugriff
Da /mnt/data im Jellyfin-Container dem Systemnutzer jellyfin gehört, wollte ich nicht
direkt darauf per Samba zugreifen - eine Vermischung von Streaming-Dienst und Dateifreigabe auf demselben
Container schien mir unnötig riskant. Stattdessen ein eigener, minimaler Container:
- 1 CPU-Kern, 512 MB-1 GB RAM genügen
- Derselbe Bind Mount auf das ZFS-Dataset wie beim Jellyfin-Container, ebenfalls unter
/mnt/data
Gemeinsame Gruppe für alle drei Container
Damit Jellyfin (lesen), Samba (lesen/schreiben) und später auch ARM (schreiben) vernünftig auf dieselben Daten zugreifen können, habe ich eine gemeinsame Gruppe mit fester GID eingerichtet - einmalig auf dem Host, dann in jedem Container identisch nachgebildet (bei privilegierten Containern werden IDs 1:1 durchgereicht):
# Auf dem Host, einmalig:
groupadd -g 3000 mediashare
chgrp -R mediashare /rpool/data
chmod -R 2775 /rpool/data
# In jedem beteiligten Container:
groupadd -g 3000 mediashare
usermod -aG mediashare jellyfin # bzw. arm, bzw. der Samba-Nutzer
Samba einrichten
sudo apt install -y samba
In /etc/samba/smb.conf:
[medien]
path = /mnt/data
browseable = yes
writable = yes
guest ok = no
valid users = andreas
force group = mediashare
create mask = 0664
directory mask = 2775
sudo smbpasswd -a andreas
sudo systemctl restart smbd
Danach ist die Freigabe unter smb://<Container-IP>/medien erreichbar - praktisch, um größere
Mengen an Dateien bequem über den Datei-Explorer statt per scp zu kopieren.
Kapitel 5: Der ARM-Container
Container anlegen
Der ARM-Container braucht ein paar Besonderheiten mehr als Jellyfin oder der Fileserver:
- Debian als Basis-Image
- Privilegiert - für das spätere USB-/SCSI-Passthrough des DVD-Laufwerks praktisch unverzichtbar
- Ausreichend Plattenplatz für temporäre Rip-Daten (ich habe 100 GB für das rootfs vorgesehen)
- Derselbe Bind Mount auf
/mnt/datawie bei Jellyfin und dem Fileserver
Eine Randnotiz zur Proxmox-GUI: Wenn man bei der Container-Erstellung "Unprivilegierter Container" deaktiviert,
wird die Checkbox für "Nesting" automatisch ausgegraut. Das ist kein Fehler, sondern gewolltes Verhalten - ein
privilegierter Container hat ohnehin nahezu alle Kernel-Capabilities, das Nesting-Flag ist nur für
unprivilegierte Container relevant. Das keyctl-Feature, das Docker ebenfalls braucht, lässt sich
bei Bedarf direkt in der Konfigurationsdatei ergänzen (Container vorher stoppen):
features: keyctl=1
Das optische Laufwerk durchreichen
Das war der aufwändigste Teil der ganzen Einrichtung. Ein einfaches Passthrough über die Proxmox-GUI reicht hier
nicht ohne Weiteres, weil ARM über pyudev zusätzliche Geräte-Eigenschaften abfragt, die nur zur
Verfügung stehen, wenn auch der udev-Datenbestand des Hosts in den Container gespiegelt wird.
Laufwerk auf dem Host identifizieren:
lsusb
dmesg | tail -30
ls -la /dev/sr* /dev/scd* 2>/dev/null
lsscsi -g
Wichtig ist neben dem Block-Device /dev/sr0 auch das zugehörige SCSI-Generic-Gerät (z. B.
/dev/sg1), über das MakeMKV die eigentlichen Lesebefehle schickt - beide werden gebraucht.
Container stoppen und in /etc/pve/lxc/<CTID>.conf ergänzen:
lxc.mount.auto: sys:rw
lxc.cgroup2.devices.allow: b 11:0 rwm
lxc.cgroup2.devices.allow: c 21:* rwm
lxc.mount.entry: /dev/sr0 dev/sr0 none bind,create=file,optional 0 0
lxc.mount.entry: /dev/sg1 dev/sg1 none bind,create=file,optional 0 0
lxc.mount.entry: /run/udev /run/udev none bind,optional,ro 0 0
Die letzte Zeile war der entscheidende Punkt: Ohne den Bind Mount von /run/udev bekommt ARM zwar
das Gerät zu sehen, aber nicht die vollständigen udev-Eigenschaften, die es für die Disc-Erkennung braucht.
Nach dem Neustart lässt sich das prüfen:
udevadm info --query=property --name=/dev/sr0
Docker und Docker Compose
ARM wird offiziell als Docker-Image bereitgestellt:
curl -fsSL https://get.docker.com | sudo bash
sudo docker run hello-world
docker compose version
Falls das Compose-Plugin fehlt:
sudo apt install -y docker-compose-plugin
Eigener Systemnutzer und Verzeichnisse
sudo groupadd arm
sudo useradd -m arm -g arm
sudo usermod -aG cdrom,video,docker,mediashare arm
sudo mkdir -p /home/arm/{logs,config}
sudo chown -R arm:arm /home/arm
docker-compose.yml
services:
arm-rippers:
image: automaticrippingmachine/automatic-ripping-machine:latest
container_name: arm-rippers
restart: always
privileged: true
ports:
- "8080:8080"
environment:
- ARM_UID=1001
- ARM_GID=1001
volumes:
- /home/arm:/home/arm
- /home/arm/logs:/home/arm/logs
- /home/arm/config:/etc/arm/config
- /mnt/data:/home/arm/media
devices:
- /dev/sr0:/dev/sr0
- /dev/sg1:/dev/sg1
Die Werte für ARM_UID/ARM_GID ermittle ich vorher mit id -u arm und
id -g arm und trage sie fest ein. Start:
cd /home/arm/docker
sudo docker compose up -d
Danach ist das Setup unter http://<Container-IP>:8080/setup erreichbar, wo ich ein Admin-Konto
angelegt habe.
Zwei Stolpersteine bei der Ersteinrichtung
Settings-Seite war schreibgeschützt: Die Felder unter Settings → Ripper Settings
ließen sich zunächst nicht bearbeiten. Ursache war, dass die Datei arm.yaml im gemounteten
Config-Verzeichnis noch root gehörte - ein Nachwirken eines früheren Tests mit docker run, bei dem
der Container kurzzeitig noch als root lief, bevor er intern auf die ARM-UID umschaltet. Behoben mit:
sudo docker compose down
sudo chown -R arm:arm /home/arm
sudo docker compose up -d
MakeMKV: "Failed to open disc": Beim ersten echten Rip-Versuch brach ARM sofort mit diesem
Fehler ab. Grund war das zu diesem Zeitpunkt noch fehlende /dev/sg-Gerät (siehe oben). Nachdem ich
es sowohl in der LXC-Konfiguration als auch in der docker-compose.yml ergänzt hatte, lief der Rip
durch.
Kapitel 6: OMDb-API-Key registrieren und in ARM eintragen
Ohne einen gültigen API-Key für die Titelerkennung rippt ARM zwar trotzdem, benennt die fertigen Dateien aber nur generisch nach dem Disc-Label statt nach dem tatsächlichen Filmtitel - das wollte ich vermeiden, damit Jellyfin die Filme später automatisch korrekt erkennt.
Key registrieren
- Auf https://www.omdbapi.com/apikey.aspx aufrufen
- Den kostenlosen "FREE!"-Tarif auswählen (reicht für den privaten Gebrauch völlig aus)
- E-Mail-Adresse eintragen und Formular absenden
- Wichtig: Der Key ist nach der Registrierung noch nicht aktiv. Es kommt eine
Bestätigungsmail mit einem Aktivierungslink - erst nach dessen Anklicken funktioniert der Key. Ohne
diesen Schritt liefert ARM beim Rippen den Fehler
omdb call failed with error - HTTP Error 401: Unauthorized, was mich zunächst auf die falsche Fährte geführt hat. - Zur Kontrolle lässt sich der Key direkt testen:
Kommt JSON mit Filmdaten zurück statt einer Fehlermeldung, ist der Key aktiv.
curl "http://www.omdbapi.com/?apikey=DEIN_KEY&t=Filmtitel"
Key in ARM eintragen
In der ARM-Weboberfläche unter Settings → Ripper Settings gibt es das Feld OMDb API Key - dort den aktivierten Key eintragen und speichern. Neuere ARM-Versionen erlauben zusätzlich, unter Metadata Provider zwischen OMDb und TMDB zu wählen, falls man lieber The Movie Database als Quelle nutzen möchte.
Kapitel 7: Weitere Einstellungen in der ARM-Weboberfläche
Neben dem API-Key habe ich unter Settings → Ripper Settings folgende Werte gesetzt:
- RIPMETHOD:
mkv- verlustfreies Rippen über MakeMKV - MAINFEATURE:
true- nur der Hauptfilm wird gerippt, nicht Bonusmaterial - MINLENGTH:
600Sekunden, um Trailer/Extras auszufiltern - EJECT_ON_COMPLETE:
true, praktisch für den Workflow beim Stapel-Rippen - HandBrake-Transcoding beim Rippen: bewusst deaktiviert gelassen (dazu mehr im nächsten Kapitel)
Kapitel 8: Warum ich auf Hardware-Transcoding beim Rippen verzichte
ARM kann optional direkt beim Rippen per HandBrake transkodieren, idealerweise mit Intel-QuickSync-Beschleunigung über dieselbe iGPU, die auch Jellyfin nutzt. Ich habe das ausprobiert, bin aber davon wieder abgekommen. QSV im offiziellen ARM-Docker-Image gilt in der Community als notorisch unzuverlässig: Es gibt mehrere offene bzw. erst kürzlich geschlossene Probleme im Projekt-Issue-Tracker, die von veralteten libva-Versionen im Image bis zu Fällen reichen, in denen HandBrake QSV zwar erkennt, das eigentliche Encoding aber trotzdem fehlschlägt - oft mit der Notwendigkeit, Treiber manuell in den bereits laufenden Container nachzuinstallieren, was bei jedem Neuaufbau des Containers wieder verloren geht, sofern man es nicht in ein eigenes Dockerfile packt.
Da meine Jellyfin-Instanz die iGPU für das Transcoding beim Streamen bereits zuverlässig nutzt, rippe ich stattdessen nur verlustfrei mit MakeMKV. Die für das jeweilige Endgerät passende Transkodierung übernimmt Jellyfin ohnehin live beim Abspielen - die zusätzliche GPU-Beschleunigung beim Rippen hätte mir nur Zeit direkt nach dem Einlegen der Disc gespart, auf Kosten von deutlich mehr Konfigurationsaufwand und Fehleranfälligkeit.
Kapitel 9: Serien-DVDs und Jellyfin-Struktur
Ein Nachtrag aus der Praxis: Bei Spielfilm-DVDs funktioniert die OMDb-Erkennung zuverlässig, und die fertigen Dateien landen mit sinnvollem Titel in der Bibliothek. Bei Serien-DVDs sieht das anders aus - ARM kann einzelne Episodentitel nicht automatisch erkennen und rippt jede Titelspur als generisch benannte Datei (z. B. nach Disc-Label und Titelnummer). Jellyfin wiederum erkennt Serien ausschließlich über Ordnerstruktur und Dateinamen, nicht durch Bildanalyse. Für Serien lege ich die Dateien deshalb manuell in folgendes Schema um:
/mnt/data/serien/
└── Serientitel/
└── Season 01/
├── 01 Titel 1.mkv
├── 02 Titel 2.mkv
└── usw.
Dazu in Jellyfin eine eigene Bibliothek vom Inhaltstyp Serien (nicht "Filme") mit TheTVDB als Metadaten-Downloader anlegen. Nach einem Scan erkennt Jellyfin die Serie samt DVD-Cover der Staffel automatisch. Die Episodentiteln werden stumpf aus den Dateinamen erstellt (darum ist sinnvolles Umbenennen wichtig). Vorschaubildern werden willkürlich erstellt und Beschreibungen der Episoden fehlen. Die Verarbeitung von Serien ist also mit manuellem Aufwand verbunden und beim Ergebnis muss man leider Abstriche machen.
Fazit
Jellyfin und ARM in privilegierten LXC-Containern statt in vollständigen VMs zu betreiben, funktioniert auf
meinen bestehenden N100-Proxmox-Hosts gut und spart Ressourcen, ohne dass ich neue Hardware anschaffen
musste. Der Aufwand steckt fast ausschließlich in zwei Stellen: dem korrekten Teilen der iGPU zwischen Host und
mehreren Containern (Userspace-Treiber müssen in jedem Container einzeln installiert werden) und dem
Durchreichen des optischen Laufwerks samt udev-Eigenschaften für ARM. Wer denselben Weg geht, sollte von Anfang
an sowohl das Block-Device als auch das zugehörige SCSI-Generic-Device einplanen und /run/udev mit
einbinden - das hätte mir einige Fehlversuche erspart. Auf GPU-beschleunigtes Transcoding beim Rippen verzichte
ich bewusst zugunsten der Einfachheit, da Jellyfin diese Aufgabe beim Streamen zuverlässig übernimmt.
Wenn ich nun meine Film-DVDs in Jellyfin nutzen möchte, dann brauche ich lediglich die DVD in das Laufwerk einzulegen und etwas abzuwarten. Nach relativ kurzer Zeit (bei einem Film mit normaler Spielfilm-Länge ca. 30 Minuten) ist der Film in meiner Jellyfin-Mediathek mit einigen Metadaten, wie DVD-Coverbild, Beschreibungen und sogar Informationen zu den Darstellern verfügbar. Dank der Einstellungen auf der ARM-Weboberfläche kann ich zwischen der Originalsprache (in der Regel englisch) und deutsch für den Ton sowie diversen Sprachen für opotionale Untertitel wählen.
Da ARM in einem Container installiert ist, kann ich die Datenpartition, die hier als Bind-Mount angelegt ist, jederzeit auf der Web-GUI von Proxmox vergrößern. Dazu reichen wenige Mausklicks, solange ich in dem betreffenden Storage auf dem Host noch Kapazitäten frei habe.
Diese Seite wurde mit Unterstützung von Claude.ai erstellt.