Halcyon: trasforma Jellyfin in un videonoleggio anni '90
Scopri come installare Halcyon con Docker, configurare il reverse proxy e valutare i limiti reali di una libreria Jellyfin in stile videonoleggio anni '90.
Cosa fa Halcyon con la tua libreria Jellyfin
Halcyon Video ridisegna la tua libreria Jellyfin come un videonoleggio degli anni '90 percorribile nel browser. Ogni film che possiedi diventa una custodia su uno scaffale. Percorri i corridoi illuminati dai neon, prendi una custodia, la giri per leggere le specifiche sul retro e la porti al bancone per avviare la riproduzione. Gli eventi di avvio, avanzamento e arresto della riproduzione vengono inviati a Jellyfin, così i punti di ripresa e la cronologia delle visualizzazioni restano corretti.
Halcyon legge un server Jellyfin esistente tramite la Jellyfin API e non mantiene una propria libreria. Questa guida presuppone che Jellyfin sia già in esecuzione e che la scansione della libreria termini senza errori. In caso contrario, configura prima Jellyfin come server multimediale su un VPS e torna qui quando la libreria viene visualizzata correttamente nel normale client web. È un servizio da installare perché la libreria esiste già, non perché ti serva un altro servizio nel tuo elenco di servizi self-hosted.
Il progetto è distribuito con licenza GPL-3.0 ed è sviluppato da una sola persona; il README dichiara chiaramente che non accetta pull request. Lo sviluppo procede rapidamente e non esiste un secondo maintainer che possa individuare una regressione, quindi blocca la versione dell'immagine prima di mostrare il videonoleggio ad altre persone. L'ultima sezione spiega come farlo.
Dove avviene il rendering?
Nel browser. Halcyon è un'applicazione Vite e TypeScript basata su three.js, una libreria JavaScript che disegna grafica 3D tramite WebGL (web graphics library, l'interfaccia del browser verso la GPU). La geometria del negozio e le immagini delle confezioni vengono composte dal dispositivo a cui è collegato lo schermo.
Il container fa molto poco. Esegue npm run serve, che è vite preview --port 1420 --strictPort --host, e distribuisce i file compilati insieme ad alcune route middleware di piccole dimensioni. Halcyon non esegue transcodifica e non esegue alcun engine sul server.
La questione della GPU riguarda quindi il client. Un piccolo VPS gestisce senza problemi questo carico, perché deve distribuire file statici e alcune route leggere tramite HTTP. È il laptop, il tablet o il televisore su cui viene eseguito il browser a determinare se il negozio funziona fluidamente o procede a scatti.
Una funzionalità fa eccezione a questa regola. Le istanze private di Remote Play avviano Chromium in modalità headless sul server, una istanza per visitatore, e trasmettono il negozio renderizzato a un telefono o a un set top box tramite WebRTC (web real time communication). In questo caso il rendering avviene sul server, con un limite predefinito di due istanze, modificabile tramite REMOTE_PLAY_MAX_INSTANCES. Anche il server deve disporre di una GPU. L'immagine pubblicata non include un renderer software; pertanto, senza un nodo di rendering /dev/dri mappato, il server rifiuta di avviare un negozio privato e risponde con This store server has no GPU available, so it can’t render a private store.
Cosa legge il negozio dalla tua libreria
Gli scaffali derivano dalla struttura interna di Jellyfin. Halcyon organizza le sezioni in base alle librerie e ai generi e raggruppa i sequel provenienti dai BoxSets. Le specifiche riportate sul retro di ogni custodia derivano dai metadati MediaStreams già presenti in Jellyfin. Di conseguenza, ciò che manca in Jellyfin manca anche sullo scaffale.
Il negozio rispecchia quindi fedelmente i tuoi metadati. Una libreria alimentata da uno stack arr in Docker Compose, con artwork e generi già compilati, offre qui un risultato molto migliore rispetto a una cartella di file sparsi con nomi generici. Le librerie fotografiche dipendono allo stesso modo dal sistema che le ha indicizzate. È un aspetto da ricordare quando confronti PhotoPrism e Immich per le immagini archiviate sullo stesso server.
Prova la demo del videonoleggio prima di installare qualsiasi componente
Il progetto mette a disposizione una demo che funziona senza server e senza accesso. Nella versione attuale del codice sorgente, la build pubblicata riempie gli scaffali con titoli in streaming, non con una libreria simile alla tua. Usala quindi per farti un’idea del negozio. Aggiungendo ?demo=1 all’URL della tua installazione, viene caricata invece una libreria sintetica.
Questa libreria sintetica serve a testare l’hardware, con una particolarità: si adatta al dispositivo. Un browser con ampia capacità GPU disponibile riceve il negozio completo, con circa 2,000 titoli. Una GPU di fascia media riceve circa 800 titoli. Un telefono, oppure qualsiasi browser che segnala 4 GB o meno di memoria del dispositivo, riceve circa 300 titoli. Se la demo funziona senza problemi su un dispositivo poco potente, si è già ridotta automaticamente. Questo dice poco sulle prestazioni della tua libreria completa sullo stesso dispositivo. Il test utile va eseguito su un dispositivo che riceve il negozio completo. Se la visualizzazione procede a scatti, anche la tua libreria di dimensioni simili procederà a scatti. In questo caso, la soluzione è la modalità 2.5D descritta di seguito, non un VPS più potente.
Eseguirlo con Docker
Questo è il comando documentato dal progetto upstream.
docker run -d --name halcyon --network host --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-videoVerificate quindi che il servizio sia stato avviato.
docker logs halcyon
curl -I http://127.0.0.1:1420Il log dovrebbe mostrare il server di anteprima in ascolto sulla porta 1420 e curl dovrebbe rispondere a HTTP/1.1 200 OK. Se un container termina dopo pochi secondi, quasi sempre la causa è la porta. --strictPort indica che il server non passa a 1421 quando 1420 è occupata e quindi si arresta.
--network host serve per Remote Play, non per lo store. WebRTC deve comunicare l'indirizzo reale della macchina al dispositivo che richiede lo streaming. Con la rete bridge predefinita di Docker, il container conosce soltanto il proprio indirizzo 172.x, che nessun telefono della rete può raggiungere. Di conseguenza, lo streaming non si connette mai. Se volete usare lo store soltanto in un browser, pubblicate invece la porta.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-videoQuesta è l'impostazione predefinita più adatta su un VPS, perché la rete dell'host inserisce il container in tutte le interfacce disponibili sulla macchina, inclusa quella pubblica. Eseguire Docker su un VPS illustra il resto di questo compromesso. --restart unless-stopped consente di riavviare lo store dopo un reboot, con lo stesso principio dei servizi Compose avviati al boot.
Clonare il repository ed eseguire docker compose up -d crea invece l'immagine localmente. Il file Compose incluso nel repository esegue per impostazione predefinita la build dal codice sorgente e contiene la riga image: relativa all'immagine precompilata commentata. Rimuovete il commento da quella riga se volete usare l'immagine pubblicata con Compose.
L'immagine pubblicata supporta sia linux/amd64 sia linux/arm64. Alla verifica dell'11 ottobre 2026, la versione 0.25.0 corrente riportava un digest separato per ogni piattaforma. Di conseguenza, un VPS arm64 esegue il pull con lo stesso comando usato su qualsiasi altro host. Il periodo in cui era disponibile soltanto amd64 è stato breve: il changelog ha rimosso l'immagine arm64 in v0.2.1, l'8 agosto 2026, e l'ha ripristinata, compilata su runner nativi, in v0.4.0, l'11 agosto 2026. Solo un host vincolato a un tag di quel periodo deve eseguire la build dal clone.
Configura l'indirizzo del server Jellyfin
Apri http://<host>:1420 ed effettua l'accesso usando l'indirizzo del server Jellyfin, il nome utente e la password. Il file .env.local.example nel repository è destinato soltanto allo sviluppo locale. Vite espone al codice lato client le variabili con prefisso VITE_, quindi una password Jellyfin scritta in quel file viene inclusa nel bundle JavaScript che ogni visitatore scarica. Su un server raggiungibile da altre persone, effettua l'accesso tramite l'interfaccia.
Il browser comunica direttamente con Jellyfin. Il container di Halcyon non fa da proxy per l'API Jellyfin. Prima di iniziare il troubleshooting, considera queste due conseguenze.
Per prima cosa, Jellyfin deve essere raggiungibile dal browser, non soltanto dal VPS che pubblica Halcyon. Un'istanza Jellyfin in ascolto su 127.0.0.1:8096 va bene per un test locale, ma lascia gli scaffali vuoti per tutti gli altri utenti.
In secondo luogo, la richiesta è cross-origin: proviene dall'indirizzo di Halcyon ed è destinata a quello di Jellyfin. Per impostazione predefinita, Jellyfin risponde alle richieste API con Access-Control-Allow-Origin: *, quindi non è necessaria una configurazione aggiuntiva. Se hai limitato questa impostazione o hai anteposto un proxy di autenticazione all'API Jellyfin, la console del browser mostra blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource e il catalogo viene caricato con gli scaffali vuoti.
Pubblicalo dietro un reverse proxy, con l'autenticazione davanti
vite preview è un server di anteprima. Non termina il TLS (Transport Layer Security) e non dispone di propri controlli di accesso, quindi per qualsiasi esposizione pubblica deve essere posizionato dietro nginx o 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;
}
}Per usare un nome di dominio davanti al container è necessaria un'impostazione aggiuntiva. Halcyon accetta richieste dirette a localhost, agli indirizzi IP e ai nomi della macchina su cui è in esecuzione, come protezione contro il DNS rebinding. In un container, la macchina su cui è in esecuzione è il container stesso, quindi il suo hostname non corrisponde al tuo. Una richiesta ricevuta come halcyon.example.com viene rifiutata e nella risposta viene indicato l'host rifiutato. Aggiungi quel nome.
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-videoIl valore è separato da virgole; un punto iniziale, come .example.com, corrisponde ai sottodomini, mentre all disabilita il controllo. Usa all soltanto su una macchina non raggiungibile dall'esterno.
Dopo aver pubblicato lo store tramite https://, anche l'indirizzo Jellyfin inserito al login deve essere https://. Un browser blocca una chiamata API http:// effettuata da una pagina HTTPS e la console mostra Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource. Il login fallisce senza ulteriori spiegazioni in Halcyon. Pubblica entrambi i servizi tramite TLS oppure mantienili entrambi su HTTP in chiaro all'interno di una rete privata.
Passiamo all'autenticazione. Lo store richiede le credenziali Jellyfin, quindi chiunque trovi l'URL visualizza una schermata di login. Una funzionalità modifica questo comportamento. Attivando Remote Play in Settings e poi Connection, la sessione Jellyfin viene trasferita al server, così i visitatori di /remote.html ottengono una propria istanza della tua libreria reale. Questo è lo scopo della funzionalità e significa che la riservatezza dell'URL è l'unica barriera tra Internet e i tuoi film. Se abiliti Remote Play, configura il single sign-on davanti all'intero sito con Authentik come gateway SSO self-hosted, oppure rimuovi l'hostname pubblico e accedi allo store tramite un tunnel WireGuard gestito con wg-easy.
Sono necessari anche due accorgimenti. Il reverse proxy inoltra soltanto lo store: lo stream Remote Play usa WebRTC su UDP e non passa attraverso un proxy HTTP, quindi richiede un percorso dedicato sulla porta 3478/udp e sulle porte da 49200 a 49260/udp quando viene utilizzato il relay TURN incluso. Inoltre, il docker run in chiaro precedente non conserva alcun volume, quindi il seed di Remote Play non sopravvive a docker rm. Per questo il file Compose monta un volume halcyon-data in /data e imposta REMOTE_PLAY_SEED su /data/remote-play-seed.json.
Cosa fare quando il negozio funziona male
Halcyon esegue il rendering su richiesta. Un negozio inattivo non compone alcun fotogramma e la perdita del focus della finestra interrompe il ciclo di animazione. Per questo una scheda lasciata aperta non scarica la batteria di un laptop. Questo aiuta una macchina che è appena al limite. Non risolve il problema di una macchina che non riesce a eseguire il rendering del negozio.
Per questi client è disponibile una modalità 2.5D, realizzata con semplice HTML e CSS e senza WebGL, pensata anche per hardware poco potente come un Raspberry Pi. Puoi passare dalla modalità 3D alla modalità 2.5D e viceversa dalle impostazioni o dal menu di alimentazione, senza ricaricare la pagina. In questo modo puoi provare entrambe le modalità sullo stesso dispositivo in pochi secondi. Considera però i limiti della soluzione: ad agosto 2026 l'autore ha definito la modalità piatta ancora acerba e ha dichiarato che il progetto era appena iniziato. Usala come fallback per i client meno potenti.
Quando un client è troppo poco potente per il negozio 3D, il problema è evidente. La scheda si ricarica da sola oppure il browser segnala che il contesto WebGL è andato perso, di solito mentre gli scaffali sono ancora in fase di caricamento. Passa quel dispositivo alla modalità 2.5D invece di ridurre la libreria.
Blocca l’immagine e verifica prima di eseguire il pull
Prendi sul serio questo aspetto. I tag v0.1.0–v0.3.1 sono stati pubblicati a pochi giorni di distanza l’uno dall’altro e v0.2.1 esiste solo perché il push dell’immagine per v0.2.0 non è riuscito. Da allora il ritmo non è rallentato: v0.25.0 è stato pubblicato il 7 ottobre 2026, tre giorni dopo v0.23.0. Le segnalazioni di bug sono benvenute a monte, ma le patch no; il flusso delle release rappresenta quindi lo stato di lavoro di una sola persona.
Eseguire latest con l’abitudine di docker pull significa che il contenuto dello store può cambiare in un normale martedì. Blocca l’immagine tramite digest, l’unico riferimento che non può cambiare.
docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0Questo comando stampa il digest associato al tag. Usalo al posto del tag.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-video@sha256:7fc00e619a30004d275faa79ad36d810e5a8530757132603065ba4e848dd67deL’11 ottobre 2026 quello era il digest associato al tag 0.25.0. Leggi personalmente il digest corrente invece di copiarlo e consulta le note di rilascio prima di procedere, perché una patch release può includere modifiche al layout dello store oltre alle correzioni.
FAQ
Halcyon richiede una GPU sul mio VPS?
Non per l'uso normale. Lo store viene visualizzato da three.js nel browser, quindi il rendering viene eseguito dal computer client. Il container serve i file compilati e alcune route server leggere sulla porta 1420. L'eccezione riguarda le istanze private di Remote Play, che eseguono Chromium headless sul server e trasmettono il risultato. Con l'immagine pubblicata, questo percorso richiede che /dev/dri sia mappato nel container. Senza una GPU, il server rifiuta di avviare uno store privato.
Posso eseguire Halcyon su Internet pubblico?
Solo dietro autenticazione. Lo store richiede le credenziali di Jellyfin, ma l'attivazione di Remote Play trasferisce la sessione Jellyfin al server. Di conseguenza, chiunque carichi /remote.html ottiene un'istanza della libreria reale senza effettuare l'accesso. Inserisci davanti un reverse proxy con single sign-on oppure escludi l'hostname dal DNS pubblico e raggiungi lo store tramite VPN.
Perché gli scaffali sono vuoti dopo l'accesso?
Il browser chiama direttamente l'API di Jellyfin. Jellyfin deve quindi essere raggiungibile dal browser, non soltanto dal VPS. Apri la console del browser. blocked by CORS policy indica che Jellyfin non accetta la richiesta proveniente dall'indirizzo di Halcyon. Un messaggio Mixed Content indica che la pagina usa HTTPS mentre l'indirizzo di Jellyfin inserito usa HTTP semplice.
Mi serve --network host?
Solo per Remote Play. WebRTC deve pubblicizzare l'indirizzo reale della macchina. Dietro il bridge Docker, il container può offrire soltanto un indirizzo 172.x che nessun telefono della rete può raggiungere. Per sfogliare lo store nel browser, -p 1420:1420 è sufficiente ed espone una parte molto più ridotta dell'host.
Quale tag dell'immagine devo usare?
Blocca un digest invece di latest. Leggi il digest di una versione con docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0, esegui quell'immagine tramite digest e procedi all'aggiornamento solo dopo aver letto le note di rilascio. Al 11 ottobre 2026, l'immagine pubblicata include sia linux/amd64 sia linux/arm64. Un host arm64 la scarica come qualsiasi altra immagine e non deve eseguire la build dal clone.