SSD Nodes Learn Hosting plans →
Anleitungen Matt ConnorVon Matt Connor

Halcyon: Jellyfin als Videothek der 90er im Browser

Halcyon macht aus Ihrer Jellyfin-Bibliothek eine begehbare Videothek der 1990er. Anleitung mit Docker-Befehl, Reverse Proxy und ehrlichen Einschränkungen.

Was Halcyon mit Ihrer Jellyfin-Bibliothek macht

Halcyon Video stellt Ihre Jellyfin-Bibliothek im Browser als begehbares Videogeschäft der 1990er-Jahre dar. Jeder Film, den Sie besitzen, wird zu einer Hülle in einem Regal. Sie gehen unter den Leuchtstoffröhren durch die Gänge, nehmen eine Hülle aus dem Regal, drehen sie um, lesen die technischen Daten auf der Rückseite und bringen sie zur Theke, um die Wiedergabe zu starten. Start, Fortschritt und Ende der Wiedergabe werden an Jellyfin zurückgemeldet. Dadurch bleiben Fortsetzungspunkte und der Wiedergabeverlauf korrekt.

Halcyon liest einen bestehenden Jellyfin-Server über die Jellyfin-API und verwaltet keine eigene Bibliothek. Diese Anleitung setzt voraus, dass Jellyfin bereits läuft und die Bibliothek fehlerfrei scannt. Falls das nicht der Fall ist, richten Sie zunächst Jellyfin als Medienserver auf einem VPS ein. Kehren Sie zurück, sobald Ihre Bibliothek im normalen Webclient korrekt angezeigt wird. Sie installieren diesen Dienst, weil die Bibliothek bereits vorhanden ist, nicht weil Sie einen weiteren Dienst für Ihre Self-Hosting-Liste benötigen.

Das Projekt steht unter der GPL-3.0 und wird von einer einzelnen Person entwickelt. In der README steht ausdrücklich, dass keine Pull Requests angenommen werden. Die Entwicklung schreitet schnell voran, und es gibt keine zweite betreuende Person, die Regressionen auffangen kann. Legen Sie daher die Image-Version fest, bevor Sie das Videogeschäft anderen zeigen. Der letzte Abschnitt beschreibt das Vorgehen.

Wo findet das Rendering statt?

Im Browser. Halcyon ist eine mit Vite und TypeScript entwickelte Anwendung auf Basis von three.js, einer JavaScript-Bibliothek, die 3D-Grafiken über WebGL (Web Graphics Library, die Schnittstelle des Browsers zur GPU) darstellt. Die Store-Geometrie und die Box-Art werden von dem Gerät zusammengesetzt, an dem der Bildschirm angeschlossen ist.

Der Container übernimmt nur sehr wenig. Er führt npm run serve aus, also vite preview --port 1420 --strictPort --host, und stellt die erstellten Dateien sowie einige kleine Middleware-Routen bereit. Halcyon führt keine Transcodierung durch und startet keine Engine auf dem Server.

Die GPU-Frage betrifft daher den Client. Ein kleiner VPS kann diese Aufgabe problemlos übernehmen, weil dabei statische Dateien und einige schlanke Routen über HTTP bereitgestellt werden. Der Laptop, das Tablet oder das Fernsehgerät, auf dem der Browser läuft, entscheidet, ob sich der Store flüssig bewegt oder nur langsam reagiert.

Eine Funktion bildet eine Ausnahme. Remote Play startet auf dem Server für jeden Besucher eine eigene headless Chromium-Instanz und überträgt den gerenderten Store per WebRTC (Web Real-Time Communication) an ein Smartphone oder eine Set-Top-Box. Bei diesem Ablauf findet das Rendering auf dem Server statt. Standardmäßig sind maximal zwei Instanzen zulässig; der Wert lässt sich mit REMOTE_PLAY_MAX_INSTANCES anpassen. Dafür wird dort ebenfalls eine GPU benötigt. Das veröffentlichte Image enthält keinen Software-Renderer. Ohne einen eingebundenen /dev/dri-Renderknoten verweigert der Server daher den Start eines privaten Stores und antwortet mit This store server has no GPU available, so it can’t render a private store.

Was der Store aus Ihrer Bibliothek liest

Die Gänge stammen aus der Struktur von Jellyfin. Halcyon erstellt Bereiche aus Ihren Bibliotheken und Genres und gruppiert Fortsetzungen anhand Ihrer BoxSets. Die technischen Angaben auf der Rückseite jeder Hülle stammen aus den MediaStreams-Metadaten, die Jellyfin bereits gespeichert hat. Was in Jellyfin fehlt, fehlt daher auch im Regal.

Damit ist der Store ein realitätsgetreues Abbild Ihrer Metadaten. Eine Bibliothek, die über einen arr-Stack in Docker Compose mit bereits hinterlegten Artworks und Genres versorgt wird, sieht hier deutlich besser aus als ein Verzeichnis mit einzelnen Dateien und generischen Namen. Fotobibliotheken hängen ebenfalls von der Software ab, die sie indiziert hat. Das sollten Sie berücksichtigen, wenn Sie PhotoPrism mit Immich für die Fotos auf demselben Server vergleichen.

Probieren Sie die Videostore-Demo aus, bevor Sie etwas installieren

Das Projekt stellt eine Demo bereit, die ohne Server und ohne Anmeldung läuft. In der aktuellen Quelle bestückt diese gehostete Version ihre Regale mit Streaming-Titeln und nicht mit einer Bibliothek, die Ihrer eigenen entspricht. Betrachten Sie sie daher als Ansicht des Stores. Wenn Sie ?demo=1 an die URL Ihrer eigenen Bereitstellung anhängen, wird stattdessen eine synthetische Bibliothek geladen.

Diese synthetische Bibliothek dient als Hardwaretest. Dabei gibt es eine Einschränkung: Ihre Größe wird an das Gerät angepasst. Ein Browser mit ausreichenden GPU-Reserven erhält den vollständigen Store mit etwa 2,000 Titeln. Eine mittelstarke GPU erhält etwa 800. Ein Smartphone oder ein Browser, der 4 GB Gerätespeicher oder weniger meldet, erhält etwa 300. Eine Demo, die auf einem schwachen Gerät flüssig läuft, hat sich bereits selbst verkleinert. Sie sagt daher wenig darüber aus, wie Ihre vollständige Bibliothek dort laufen wird. Aussagekräftig ist ein Gerät, das den vollständigen Store erhält. Wenn die Darstellung dort ruckelt, wird auch Ihre eigene Bibliothek ähnlicher Größe ruckeln. Die Lösung ist dann der weiter unten beschriebene 2.5D-Modus und nicht ein größerer VPS.

Mit Docker ausführen

Dies ist der vom Upstream dokumentierte Befehl.

docker run -d --name halcyon --network host --restart unless-stopped \
  ghcr.io/halcyon-video/halcyon-video

Prüfen Sie anschließend, ob der Dienst gestartet ist.

docker logs halcyon
curl -I http://127.0.0.1:1420

Das Log sollte zeigen, dass der Preview-Server auf Port 1420 lauscht, und curl sollte auf HTTP/1.1 200 OK antworten. Ein Container, der innerhalb weniger Sekunden beendet wird, scheitert fast immer am Port. --strictPort bedeutet, dass der Server nicht auf 1421 ausweicht, wenn 1420 bereits belegt ist, und stattdessen beendet wird.

--network host wird für Remote Play benötigt, nicht für den Store. WebRTC muss dem Gerät, das den Stream anfordert, die tatsächliche Adresse des Rechners mitteilen. Hinter der standardmäßigen Docker-Bridge kennt der Container nur seine eigene 172.x-Adresse. Kein Telefon in Ihrem Netzwerk kann diese Adresse erreichen, daher wird der Stream nie verbunden. Wenn Sie den Store nur in einem Browser verwenden möchten, veröffentlichen Sie stattdessen den Port.

docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
  ghcr.io/halcyon-video/halcyon-video

Das ist auf einem VPS die bessere Standardeinstellung, weil das Host-Netzwerk den Container auf jede Schnittstelle des Rechners legt, einschließlich der öffentlichen Schnittstelle. Docker auf einem VPS ausführen behandelt die übrigen Aspekte dieses Abwägens. --restart unless-stopped sorgt dafür, dass der Store nach einem Reboot wieder gestartet wird, nach demselben Prinzip wie Compose-Dienste, die beim Booten starten.

Das Klonen des Repositorys und das Ausführen von docker compose up -d erstellt das Image stattdessen lokal. Die versionierte Compose-Datei erstellt standardmäßig das Image aus dem Quellcode und enthält die vorkompilierte image:-Zeile auskommentiert. Entfernen Sie die Auskommentierung dieser Zeile, wenn Sie unter Compose das veröffentlichte Image verwenden möchten.

Das veröffentlichte Image unterstützt sowohl linux/amd64 als auch linux/arm64. Bei einer Prüfung am 11. Oktober 2026 listete die aktuelle 0.25.0-Version einen separaten Digest für jede Plattform. Daher wird ein arm64-VPS mit demselben Befehl wie jeder andere Host versorgt. Die kurze Phase, in der nur amd64 unterstützt wurde, war begrenzt: Im Changelog wurde das arm64-Image in v0.2.1 am 8. August 2026 entfernt und in v0.4.0 am 11. August 2026, erstellt auf nativen Runnern, wieder hinzugefügt. Nur ein Host, der auf einen Tag aus diesem Zeitraum festgelegt ist, muss aus dem Klon erstellen.

Verweisen Sie auf Ihren Jellyfin-Server

Öffnen Sie http://<host>:1420 und melden Sie sich mit der Adresse, dem Benutzernamen und dem Passwort Ihres Jellyfin-Servers an. Die Datei .env.local.example im Repository ist nur für die lokale Entwicklung vorgesehen. Vite stellt Variablen mit dem Präfix VITE_ im clientseitigen Code bereit. Ein dort eingetragenes Jellyfin-Passwort wird daher in das JavaScript-Bundle kompiliert, das jeder Besucher herunterlädt. Wenn der Server für andere Personen erreichbar ist, melden Sie sich über die Benutzeroberfläche an.

Der Browser kommuniziert direkt mit Jellyfin. Der Container von Halcyon leitet die Jellyfin-API nicht weiter. Daraus ergeben sich zwei wichtige Punkte, die Sie vor der Fehlersuche kennen sollten.

Erstens muss Jellyfin vom Browser aus erreichbar sein, nicht nur von dem VPS, auf dem Halcyon bereitgestellt wird. Ein an 127.0.0.1:8096 gebundener Jellyfin-Dienst eignet sich für einen lokalen Test. Für alle anderen bleiben die Regale dann leer.

Zweitens handelt es sich um eine Cross-Origin-Anfrage von der Halcyon-Adresse an die Jellyfin-Adresse. Jellyfin beantwortet API-Anfragen standardmäßig mit Access-Control-Allow-Origin: *. Daher funktioniert die Verbindung ohne zusätzliche Konfiguration. Wenn Sie diese Einstellung eingeschränkt oder einen Authentifizierungs-Proxy vor die Jellyfin-API geschaltet haben, meldet die Browserkonsole blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Der Store wird dann mit leeren Regalen geladen.

Vor einen Reverse Proxy mit vorgeschalteter Authentifizierung setzen

vite preview ist ein Vorschau-Server. Er terminiert keine TLS-Verbindungen (Transport Layer Security) und verfügt über keine eigene Zugriffskontrolle. Deshalb gehört er bei jeder öffentlichen Bereitstellung hinter nginx oder Caddy.

server {
  listen 443 ssl;
  server_name halcyon.example.com;

  location / {
    proxy_pass http://127.0.0.1:1420;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

Für einen Domainnamen vor dem Container ist eine zusätzliche Einstellung erforderlich. Halcyon akzeptiert localhost, reine IP-Adressen und die Namen des Systems, auf dem es ausgeführt wird, als Schutz vor DNS-Rebinding. In einem Container ist der Rechner, auf dem es ausgeführt wird, der Container selbst. Sein Hostname ist daher nicht Ihr Hostname. Eine Anfrage an halcyon.example.com wird abgelehnt. Die Antwort nennt den abgelehnten Host. Fügen Sie diesen Namen hinzu.

docker run -d --name halcyon -p 127.0.0.1:1420:1420 --restart unless-stopped \
  -e HALCYON_ALLOWED_HOSTS=halcyon.example.com \
  ghcr.io/halcyon-video/halcyon-video

Der Wert ist durch Kommas getrennt. Ein führender Punkt wie .example.com gilt auch für Subdomains. all deaktiviert die Prüfung. Verwenden Sie all nur auf einem Rechner, den nichts außerhalb Ihres Netzwerks erreichen kann.

Sobald der Store über https:// bereitgestellt wird, muss auch die Jellyfin-Adresse, die Sie bei der Anmeldung eingeben, https:// lauten. Ein Browser blockiert einen unverschlüsselten http://-API-Aufruf von einer HTTPS-Seite. Die Konsole meldet Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource. Die Anmeldung schlägt einfach fehl, ohne dass Halcyon eine Erklärung anzeigt. Stellen Sie beide Dienste über TLS bereit oder verwenden Sie beide innerhalb eines privaten Netzwerks über unverschlüsseltes HTTP.

Nun zur Authentifizierung. Der Store fordert Jellyfin-Zugangsdaten an. Ein Fremder, der die URL findet, sieht daher zunächst eine Anmeldeseite. Eine Funktion ändert dieses Verhalten. Wenn Sie unter Settings und anschließend Connection die Funktion Remote Play aktivieren, übergibt Ihr Jellyfin-Login dem Server Ihre Sitzung. Besucher von /remote.html erhalten dadurch eine eigene Instanz Ihrer tatsächlichen Bibliothek. Genau dafür ist diese Funktion vorgesehen. Damit hängt die Sicherheit Ihrer Filme davon ab, dass die URL geheim bleibt. Wenn Sie Remote Play aktivieren, schalten Sie eine Single-Sign-on-Lösung vor die gesamte Website, beispielsweise Authentik als selbst gehostetes SSO-Gateway, oder entfernen Sie den öffentlichen Hostnamen und greifen Sie über einen mit wg-easy verwalteten WireGuard-Tunnel auf den Store zu.

Damit sind zwei Details verbunden. Der Reverse Proxy überträgt nur den Store. Der Remote-Play-Stream verwendet WebRTC über UDP und läuft nicht über einen HTTP-Proxy. Deshalb benötigt er einen eigenen Pfad über 3478/udp sowie über 49200 bis 49260/udp, wenn das enthaltene TURN-Relay verwendet wird. Außerdem speichert das oben genannte einfache docker run kein Volume. Der Remote-Play-Seed bleibt daher docker rm nicht erhalten. Die Compose-Datei bindet aus genau diesem Grund ein halcyon-data-Volume unter /data ein und setzt REMOTE_PLAY_SEED auf /data/remote-play-seed.json.

Was Sie tun können, wenn der Store schlecht läuft

Halcyon rendert bei Bedarf. Ein inaktiver Store rendert keine Frames. Wenn der Fensterfokus verloren geht, stoppt die Animationsschleife. Deshalb leert ein geöffnetes Tab nicht den Akku eines Laptops. Das hilft bei einem Gerät, dessen Leistung gerade noch ausreicht. Es hilft nicht bei einem Gerät, das den Store überhaupt nicht darstellen kann.

Für solche Clients gibt es einen 2.5D-Modus. Er verwendet reines HTML und CSS ohne WebGL und ist für Hardware bis hin zu einem Raspberry Pi ausgelegt. Sie wechseln in den Einstellungen oder im Energiemenü ohne Neuladen der Seite zwischen 3D und 2.5D. Dadurch können Sie beide Modi auf demselben Gerät innerhalb weniger Sekunden testen. Bleiben Sie bei den Erwartungen realistisch: Im August 2026 bezeichnete der Autor den flachen Modus als unausgereift und erklärte, das Projekt habe gerade erst begonnen. Betrachten Sie ihn als Ausweichlösung für leistungsschwache Clients.

Wenn ein Client für den 3D-Store zu leistungsschwach ist, fällt der Fehler deutlich auf. Das Tab lädt sich selbst neu, oder der Browser meldet einen verlorenen WebGL-Kontext. Das geschieht normalerweise, während die Regale noch gefüllt werden. Stellen Sie das Gerät auf 2.5D um, statt Ihre Bibliothek zu verkleinern.

Das Image festschreiben und vor dem Abruf prüfen

Nehmen Sie diesen Abschnitt ernst. Die Tags v0.1.0 bis v0.3.1 wurden innerhalb weniger Tage veröffentlicht, und v0.2.1 existiert nur, weil der Image-Push für v0.2.0 fehlgeschlagen ist. Das Tempo hat seitdem nicht nachgelassen: v0.25.0 wurde am 7. Oktober 2026 veröffentlicht, drei Tage nach v0.23.0. Fehlerberichte sind upstream willkommen, Patches jedoch nicht. Der Release-Zweig entspricht daher dem Arbeitsstand einer einzelnen Person.

Wenn Sie latest regelmäßig mit docker pull ausführen, kann sich der Inhalt des Stores an jedem beliebigen Dienstag unter Ihrer Umgebung ändern. Verwenden Sie den Digest als feste Referenz. Nur diese Referenz kann nicht auf ein anderes Image zeigen.

docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0

Damit wird der Digest hinter dem Tag ausgegeben. Verwenden Sie ihn anstelle des Tags.

docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
  ghcr.io/halcyon-video/halcyon-video@sha256:7fc00e619a30004d275faa79ad36d810e5a8530757132603065ba4e848dd67de

Am 11. Oktober 2026 war dies der Digest hinter dem Tag 0.25.0. Lesen Sie den aktuellen Digest selbst aus, statt ihn zu kopieren. Lesen Sie außerdem die Release Notes, bevor Sie wechseln, da ein Patch-Release hier neben Fehlerkorrekturen auch Änderungen am Store-Layout enthalten kann.

FAQ

Benötigt Halcyon eine GPU auf meinem VPS?

Für den normalen Betrieb nicht. Der Store wird mit three.js im Browser dargestellt. Daher übernimmt der Client die Darstellung, während der Container die erstellten Dateien und einige einfache Server-Routen auf Port 1420 bereitstellt. Eine Ausnahme sind die privaten Instanzen von Remote Play. Sie führen Chromium im Headless-Modus auf dem Server aus und streamen das Ergebnis. Beim veröffentlichten Image muss dafür /dev/dri in den Container eingebunden werden. Ohne GPU verweigert der Server den Start eines privaten Stores.

Kann ich Halcyon im öffentlichen Internet bereitstellen?

Nur hinter einer Authentifizierung. Der Store fragt nach Jellyfin-Zugangsdaten. Wenn Sie Remote Play aktivieren, übergeben Sie jedoch Ihre Jellyfin-Sitzung an den Server. Dadurch erhält jeder, der /remote.html aufruft, ohne Anmeldung eine Instanz Ihrer echten Bibliothek. Schalten Sie einen Reverse Proxy mit Single Sign-On davor. Alternativ können Sie den Hostnamen aus dem öffentlichen DNS heraushalten und den Store über ein VPN erreichen.

Warum sind die Regale leer, nachdem ich mich angemeldet habe?

Der Browser ruft die Jellyfin-API direkt auf. Jellyfin muss daher vom Browser aus erreichbar sein und darf nicht nur vom VPS aus erreichbar sein. Öffnen Sie die Browserkonsole. blocked by CORS policy bedeutet, dass Jellyfin die Anfrage von der Adresse von Halcyon nicht akzeptiert. Eine Mixed Content-Meldung bedeutet, dass die Seite HTTPS verwendet, während die eingegebene Jellyfin-Adresse reines HTTP ist.

Benötige ich --network host?

Nur für Remote Play. WebRTC muss die tatsächliche Adresse des Rechners bekannt geben. Hinter der Docker-Bridge kann der Container jedoch nur eine 172.x-Adresse anbieten, die kein Telefon in Ihrem Netzwerk erreichen kann. Für das Durchsuchen des Stores im Browser reicht -p 1420:1420 aus und legt deutlich weniger vom Host offen.

Welchen Image-Tag sollte ich verwenden?

Fixieren Sie einen Digest statt latest. Lesen Sie den Digest für eine Version mit docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0 aus. Verwenden Sie diesen Digest und wechseln Sie erst danach, wenn Sie die Release Notes gelesen haben. Am 11. Oktober 2026 enthält das veröffentlichte Image sowohl linux/amd64 als auch linux/arm64. Ein arm64-Host kann es daher wie jedes andere Image abrufen und muss es nicht aus dem Clone erstellen.