SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-22

Halcyon transforme Jellyfin en vidéoclub des années 90

Découvrez comment installer Halcyon avec Docker, le relier à Jellyfin via un reverse proxy et connaître ses limites avant de remplacer votre client web.

Ce que Halcyon fait de votre bibliothèque Jellyfin

Halcyon Video transforme votre bibliothèque Jellyfin en un vidéoclub des années 1990 que vous pouvez parcourir dans le navigateur. Chaque film que vous possédez devient une jaquette sur une étagère. Vous parcourez les rayons sous les néons, prenez une jaquette, la retournez pour lire les informations au dos, puis l’apportez au comptoir pour démarrer la lecture. Les événements de lecture — démarrage, progression et arrêt — sont transmis à Jellyfin. Les points de reprise et l’historique de visionnage restent donc corrects.

Halcyon lit un serveur Jellyfin existant via l’API Jellyfin et ne conserve aucune bibliothèque locale. Ce guide part du principe que Jellyfin fonctionne déjà et que l’indexation se déroule correctement. Si ce n’est pas le cas, configurez d’abord Jellyfin comme serveur multimédia sur un VPS, puis revenez une fois que votre bibliothèque s’affiche correctement dans le client web standard. Vous installez cet outil parce que la bibliothèque existe déjà, et non parce que vous aviez besoin d’un service supplémentaire dans votre liste de services auto-hébergés.

Le projet est sous licence GPL-3.0 et est développé par une seule personne. Le README indique clairement que les pull requests ne sont pas acceptées. Le développement avance rapidement et aucun second mainteneur ne peut détecter une régression. Épinglez donc la version de l’image avant de montrer le vidéoclub à d’autres personnes. La dernière section explique comment procéder.

Où le rendu est-il effectué ?

Dans le navigateur. Halcyon est une application Vite et TypeScript basée sur three.js, une bibliothèque JavaScript qui affiche des graphismes 3D avec WebGL (web graphics library, l’interface du navigateur avec le GPU). La géométrie du magasin et les illustrations des boîtes sont composées par la machine à laquelle l’écran est connecté.

Le conteneur fait très peu de choses. Il exécute npm run serve, qui est vite preview --port 1420 --strictPort --host, et sert les fichiers compilés ainsi que quelques routes de middleware. Halcyon n’effectue aucun transcodage et n’exécute aucun moteur sur le serveur.

La question du GPU concerne donc le client. Un petit VPS suffit largement, car le service consiste à fournir des fichiers statiques en HTTP. C’est l’ordinateur portable, la tablette ou le téléviseur qui exécute le navigateur qui détermine si le magasin s’affiche de manière fluide ou saccadée.

Une fonctionnalité fait exception à cette règle. Remote Play lance des instances Chromium headless sur le serveur et diffuse le magasin rendu vers un téléphone ou un set-top box via WebRTC (web real time communication). Dans ce cas, le rendu est effectué sur le serveur. Le nombre d’instances est limité à deux par défaut et peut être modifié avec REMOTE_PLAY_MAX_INSTANCES. Sans périphérique /dev/dri associé, ces instances effectuent le rendu sur le CPU. Un VPS à deux cœurs ressent donc chaque utilisateur supplémentaire.

Ce que la boutique lit dans votre bibliothèque

Les rayons reprennent la structure de Jellyfin. Halcyon organise les sections à partir de vos bibliothèques et de vos genres, puis regroupe les suites provenant de vos BoxSets. Les caractéristiques imprimées au dos de chaque boîtier proviennent des métadonnées MediaStreams que Jellyfin conserve déjà. Toute information absente de Jellyfin est donc absente du rayon.

La boutique reflète ainsi fidèlement vos métadonnées. Une bibliothèque alimentée par une stack arr dans Docker Compose, avec les illustrations et les genres déjà renseignés, donne ici un bien meilleur résultat qu’un dossier de fichiers isolés aux noms génériques. Les photothèques dépendent de la même manière de l’outil qui les a indexées. Gardez ce point à l’esprit lorsque vous comparez PhotoPrism et Immich pour les photos stockées sur le même serveur.

Tester la démo de la vidéothèque avant toute installation

Le projet publie la vidéothèque complète avec une bibliothèque synthétique dans la démo hébergée. Ajoutez ?demo=1 à n’importe quelle URL Halcyon pour obtenir le même résultat sur votre propre déploiement.

Utilisez-la pour tester le matériel. La bibliothèque de démonstration contient environ 2,000 titres et nécessite environ 2 GB de mémoire du navigateur, ce qui est plus exigeant que la plupart des bibliothèques personnelles. Si la démo saccade sur l’appareil depuis lequel vous comptez naviguer, votre propre bibliothèque saccadera aussi. La solution est alors le mode 2.5D décrit ci-dessous, et non un VPS plus puissant.

L’exécuter avec Docker

Voici la commande documentée par le projet en amont.

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

Vérifiez ensuite qu’il a bien démarré.

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

Le journal doit indiquer que le serveur de prévisualisation écoute sur le port 1420, et curl doit répondre à HTTP/1.1 200 OK. Un conteneur qui s’arrête après quelques secondes est presque toujours confronté à un problème de port. --strictPort signifie que le serveur refuse de basculer sur 1421 lorsque 1420 est déjà utilisé. Il s’arrête donc à la place.

--network host sert à Remote Play, pas au store. WebRTC doit annoncer l’adresse réelle de la machine à l’appareil qui demande le flux. Derrière le bridge Docker par défaut, le conteneur ne connaît que sa propre adresse 172.x. Aucun téléphone de votre réseau ne peut l’atteindre. Le flux ne se connecte donc jamais. Si vous voulez uniquement utiliser le store dans un navigateur, publiez plutôt le port.

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

C’est le meilleur choix par défaut sur un VPS, car le réseau de l’hôte place le conteneur sur toutes les interfaces de la machine, y compris l’interface publique. Exécuter Docker sur un VPS présente les autres conséquences de ce choix. --restart unless-stopped permet de relancer le store après un redémarrage, selon le même principe que les services Compose lancés au démarrage.

Cloner le dépôt et exécuter docker compose up -d construit l’image localement. Le fichier Compose versionné construit l’image depuis les sources par défaut et contient la ligne image: de l’image préconstruite commentée. Décommentez cette ligne si vous voulez utiliser l’image publiée avec Compose.

Une limite importante s’applique en août 2026 : l’image publiée est uniquement linux/amd64. La partie arm64 de la publication multi-architecture a échoué sous émulation et attend des runners arm natifs. Sur un VPS arm64, le téléchargement échoue avec no matching manifest for linux/arm64/v8 in the manifest list entries. Construire l’image depuis le clone permet de contourner ce problème.

Pointez-le vers votre serveur Jellyfin

Ouvrez http://<host>:1420 et connectez-vous avec l’adresse de votre serveur Jellyfin, votre nom d’utilisateur et votre mot de passe. Le fichier .env.local.example du dépôt est réservé au développement local. Vite expose les variables préfixées par VITE_ au code côté client. Un mot de passe Jellyfin écrit à cet emplacement est donc intégré au bundle JavaScript téléchargé par chaque visiteur. Sur un serveur accessible à d’autres personnes, connectez-vous via l’interface.

Le navigateur communique directement avec Jellyfin. Le conteneur Halcyon ne fait pas proxy pour l’API Jellyfin. Deux conséquences sont à connaître avant de commencer le diagnostic.

Premièrement, Jellyfin doit être accessible depuis le navigateur, et pas seulement depuis le VPS qui sert Halcyon. Un Jellyfin lié à 127.0.0.1:8096 convient pour un test local, mais les étagères restent vides pour les autres utilisateurs.

Deuxièmement, l’appel est cross-origin : il part de l’adresse de Halcyon vers celle de Jellyfin. Par défaut, Jellyfin répond aux requêtes API avec Access-Control-Allow-Origin: *. Aucune configuration supplémentaire n’est donc nécessaire. Si vous avez restreint ce paramètre ou placé un proxy d’authentification devant l’API Jellyfin, la console du navigateur signale blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource et le magasin se charge avec des étagères vides.

Placez-le derrière un reverse proxy, avec une authentification en amont

vite preview est un serveur de prévisualisation. Il ne gère pas la terminaison TLS (Transport Layer Security) et ne fournit aucun contrôle d’accès, il doit donc être placé derrière nginx ou Caddy dès qu’il est exposé publiquement.

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;
  }
}

L’utilisation d’un nom de domaine devant le conteneur nécessite un réglage supplémentaire. Halcyon accepte les requêtes provenant de localhost, d’adresses IP brutes et des noms de la machine sur laquelle il s’exécute, afin de se protéger contre le DNS rebinding. Dans un conteneur, la machine sur laquelle il s’exécute est le conteneur lui-même. Son hostname n’est donc pas le vôtre. Une requête reçue avec halcyon.example.com est refusée, et la réponse indique le host refusé. Ajoutez ce nom.

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

La valeur est une liste séparée par des virgules. Un point initial, comme .example.com, correspond aux sous-domaines. La valeur all désactive le contrôle. Utilisez all uniquement sur une machine inaccessible depuis l’extérieur.

Une fois le store servi via https://, l’adresse Jellyfin saisie lors de la connexion doit également être https://. Un navigateur bloque un appel API en http:// effectué depuis une page HTTPS, et la console affiche Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource. La connexion échoue simplement, sans explication dans Halcyon. Servez les deux via TLS, ou utilisez HTTP en clair pour les deux au sein d’un réseau privé.

Passons à l’authentification. Le store demande les identifiants Jellyfin. Un inconnu qui trouve l’URL arrive donc sur un écran de connexion. Une fonctionnalité modifie ce comportement. L’activation de Remote Play, dans Settings puis Connection, transmet votre session Jellyfin au serveur. Les visiteurs de /remote.html accèdent ainsi à leur propre instance de votre véritable bibliothèque. C’est le rôle de cette fonctionnalité. La confidentialité de l’URL devient alors la seule protection entre Internet et vos films. Si vous activez Remote Play, placez un système de single sign-on devant l’ensemble du site avec Authentik comme passerelle SSO auto-hébergée, ou supprimez le hostname public et accédez au store via un tunnel WireGuard géré avec wg-easy.

Deux points sont à prendre en compte. Le reverse proxy ne transporte que le store. Le flux Remote Play utilise WebRTC sur UDP et ne passe pas par un proxy HTTP. Il nécessite donc son propre chemin sur 3478/udp et sur 49200 à 49260/udp lorsque le relais TURN inclus est utilisé. De plus, le docker run en clair défini plus haut ne conserve aucun volume. Le seed Remote Play ne survit donc pas à docker rm. Le fichier Compose monte un volume halcyon-data sur /data et définit REMOTE_PLAY_SEED sur /data/remote-play-seed.json précisément pour cette raison.

Que faire lorsque la boutique fonctionne mal

Halcyon effectue le rendu à la demande. Une boutique inactive ne compose aucune image, et la perte du focus de la fenêtre arrête la boucle d’animation. C’est pourquoi un onglet laissé ouvert ne vide pas la batterie d’un ordinateur portable. Cela aide une machine qui atteint tout juste les limites requises. En revanche, cela ne change rien pour une machine incapable d’afficher la boutique.

Pour ces clients, un mode 2.5D est disponible. Il utilise uniquement du HTML et du CSS, sans WebGL, et vise du matériel aussi peu puissant qu’un Raspberry Pi. Vous pouvez passer de la 3D à la 2.5D depuis les paramètres ou le menu d’alimentation, sans recharger la page. Tester les deux modes sur le même appareil ne prend donc que quelques secondes. Restez réaliste quant au résultat : l’auteur décrit le mode plat comme rudimentaire et encore en cours de développement. Considérez-le comme une solution de repli pour les clients peu puissants.

Lorsqu’un client est trop limité pour la boutique 3D, l’échec est évident. L’onglet se recharge tout seul ou le navigateur signale une perte du contexte WebGL, généralement pendant le chargement des rayons. Basculez cet appareil en mode 2.5D au lieu de réduire votre bibliothèque.

Figez l’image et vérifiez-la avant de la télécharger

Prenez cette partie au sérieux. Les tags v0.1.0 à v0.3.1 sont tous sortis à quelques jours d’intervalle, et v0.2.1 n’existe que parce que le push de l’image pour v0.2.0 a échoué. Les rapports de bugs sont les bienvenus en amont, mais pas les patchs. Le flux de versions correspond donc à l’état de travail d’une seule personne.

Exécuter latest avec l’habitude de docker pull signifie que le store peut changer lors de n’importe quel mardi ordinaire. Figez l’image avec son digest, la seule référence qui ne peut pas changer.

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

Cette commande affiche le digest associé au tag. Utilisez-le à la place du tag.

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

Ce digest était 0.3.1 le 10 août 2026. Lisez vous-même la valeur actuelle au lieu de la copier, et consultez les notes de version avant de mettre à niveau, car une version corrective peut modifier la structure du store en plus d’apporter des corrections.

FAQ

Halcyon a-t-il besoin d’un GPU sur mon VPS ?

Pas pour une utilisation normale. La boutique est rendue par three.js dans le navigateur. La machine cliente effectue donc le rendu, tandis que le conteneur sert uniquement des fichiers statiques sur le port 1420. L’exception concerne Remote Play, qui exécute Chromium en mode headless sur le serveur et diffuse le résultat. Dans ce cas, le rendu utilise le CPU, sauf si vous mappez /dev/dri dans le conteneur pour activer l’accélération matérielle.

Puis-je exposer Halcyon sur Internet ?

Uniquement derrière une authentification. La boutique demande les identifiants Jellyfin, mais l’activation de Remote Play transmet votre session Jellyfin au serveur. Toute personne qui charge /remote.html obtient donc une instance de votre bibliothèque réelle sans se connecter. Placez un reverse proxy avec single sign-on devant le service, ou ne publiez pas le hostname dans le DNS public et accédez à la boutique via un VPN.

Pourquoi les étagères sont-elles vides après la connexion ?

Le navigateur appelle directement l’API Jellyfin. Jellyfin doit donc être accessible depuis le navigateur, et pas uniquement depuis le VPS. Ouvrez la console du navigateur. blocked by CORS policy signifie que Jellyfin n’accepte pas la requête provenant de l’adresse de Halcyon. Un message Mixed Content signifie que la page est en HTTPS, alors que l’adresse Jellyfin saisie utilise le HTTP en clair.

Ai-je besoin de --network host ?

Uniquement pour Remote Play. WebRTC doit annoncer l’adresse réelle de la machine. Derrière le bridge Docker, le conteneur ne peut proposer qu’une adresse 172.x qu’aucun téléphone de votre réseau ne peut atteindre. Pour parcourir la boutique dans un navigateur, -p 1420:1420 suffit et expose beaucoup moins l’hôte.

Quel tag d’image dois-je utiliser ?

Épinglez plutôt un digest que latest. Lisez le digest correspondant à une version avec docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.3.1, exécutez cette image avec ce digest, puis ne la mettez à jour qu’après avoir lu les notes de version. En août 2026, l’image publiée est uniquement linux/amd64. Un hôte arm64 doit donc effectuer le build depuis le clone avec docker compose up -d.