Halcyon: transforme o Jellyfin numa locadora dos anos 90
Veja como instalar o Halcyon com Docker, configurar o reverse proxy e conhecer as limitações de um projeto individual que recria sua biblioteca Jellyfin.
O que o Halcyon faz com a sua biblioteca do Jellyfin
O Halcyon Video transforma a sua biblioteca do Jellyfin numa loja de vídeo dos anos 1990 que pode percorrer no browser. Cada filme que possui torna-se numa caixa numa prateleira. Percorra os corredores sob as luzes fluorescentes, retire uma caixa, vire-a para ler as especificações na parte de trás e leve-a ao balcão para iniciar a reprodução. Os eventos de início, progresso e paragem da reprodução são enviados de volta para o Jellyfin, para que os pontos de retoma e o histórico de visualização permaneçam corretos.
O Halcyon lê um servidor Jellyfin existente através da API do Jellyfin e não mantém uma biblioteca própria. Este guia pressupõe que o Jellyfin já está em execução e a indexar a biblioteca sem erros. Se isso não acontecer, configure primeiro o Jellyfin como servidor de multimédia numa VPS e volte quando a biblioteca estiver correta no cliente web normal. Este é o tipo de aplicação que se instala porque a biblioteca já existe, não porque precisava de outro serviço na sua lista de self-hosting.
O projeto está licenciado sob GPL-3.0 e é desenvolvido por uma pessoa. O README declara claramente que não aceita pull requests. O desenvolvimento é rápido e não existe um segundo responsável para detetar regressões. Por isso, fixe a versão da imagem antes de mostrar a loja a outras pessoas. A última secção explica como.
Onde ocorre a renderização?
No navegador. Halcyon é uma aplicação Vite e TypeScript baseada em three.js, uma biblioteca JavaScript que desenha gráficos 3D através de WebGL (web graphics library, a interface do navegador para a GPU). A geometria da loja e as imagens das caixas são compostas pelo dispositivo ao qual o ecrã está ligado.
O contentor faz muito pouco. Executa npm run serve, que é vite preview --port 1420 --strictPort --host, e disponibiliza os ficheiros compilados, além de algumas rotas pequenas de middleware. Halcyon não faz transcodificação nem executa nenhum motor no servidor.
Por isso, a questão da GPU pertence ao cliente. Um VPS pequeno disponibiliza esta aplicação sem problemas, porque isso significa servir ficheiros estáticos através de HTTP. O portátil, tablet ou televisor que executa o navegador é que determina se a loja funciona de forma fluida ou se fica lenta.
Uma funcionalidade quebra esta regra. O Remote Play inicia instâncias headless do Chromium no servidor e transmite a loja renderizada para um telefone ou set-top box através de WebRTC (web real time communication). Nesse percurso, a renderização ocorre no servidor. Por predefinição, o limite é de duas instâncias e pode ser ajustado com REMOTE_PLAY_MAX_INSTANCES. Sem um dispositivo /dev/dri mapeado, essas instâncias renderizam na CPU. Por isso, um VPS com dois cores sente cada visualizador adicional.
O que a loja lê da sua biblioteca
Os corredores vêm da própria estrutura do Jellyfin. O Halcyon organiza as secções a partir das suas bibliotecas e géneros, e agrupa as sequelas dos seus BoxSets. As especificações impressas na parte traseira de cada caixa vêm dos metadados MediaStreams que o Jellyfin já tem, o que significa que tudo o que estiver em falta no Jellyfin também estará em falta na prateleira.
Isso faz da loja um reflexo fiel dos seus metadados. Uma biblioteca alimentada por uma stack arr no Docker Compose, com capas e géneros já preenchidos, tem aqui um aspeto muito melhor do que uma pasta com ficheiros soltos e nomes genéricos. As bibliotecas de fotografias têm a mesma dependência do sistema que as indexou. Tenha isso em conta quando comparar PhotoPrism com Immich para as fotografias armazenadas no mesmo servidor.
Experimente a demonstração da videoteca antes de instalar qualquer componente
O projeto publica a videoteca completa a funcionar com uma biblioteca sintética na demonstração alojada. Acrescentar ?demo=1 a qualquer URL do Halcyon produz o mesmo resultado na sua própria implementação.
Use-a como teste de hardware. A biblioteca de demonstração contém cerca de 2,000 títulos e necessita de aproximadamente 2 GB de memória do navegador, o que é mais exigente do que a maioria das bibliotecas pessoais. Se a demonstração apresentar falhas no dispositivo a partir do qual pretende navegar, a sua própria biblioteca também apresentará falhas. Nesse caso, use o modo 2.5D descrito abaixo em vez de um VPS maior.
Execute com Docker
Este é o comando documentado pelo projeto upstream.
docker run -d --name halcyon --network host --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-videoDepois, confirme que o serviço iniciou.
docker logs halcyon
curl -I http://127.0.0.1:1420O log deve mostrar o servidor de pré-visualização a escutar na porta 1420, e curl deve responder a HTTP/1.1 200 OK. Um contentor que termina ao fim de alguns segundos quase sempre indica um problema com a porta. --strictPort significa que o servidor recusa mudar para 1421 quando 1420 está ocupada, pelo que termina.
--network host é necessário para o Remote Play, não para a loja. O WebRTC tem de anunciar o endereço real da máquina ao dispositivo que pretende receber o stream. Por trás da bridge Docker predefinida, o contentor conhece apenas o seu próprio endereço 172.x, ao qual nenhum telefone da sua rede consegue chegar, pelo que o stream nunca estabelece ligação. Se pretende apenas utilizar a loja num browser, publique a porta.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-videoEsta é a melhor predefinição num VPS, porque a rede do host coloca o contentor em todas as interfaces da máquina, incluindo a interface pública. Executar Docker num VPS explica o restante desta decisão. --restart unless-stopped é o que faz a loja voltar a iniciar depois de um reboot, seguindo a mesma ideia de Serviços Compose que arrancam no boot.
Clonar o repositório e executar docker compose up -d cria a imagem localmente. O ficheiro Compose incluído cria a imagem a partir do código-fonte por predefinição e contém a linha image: da imagem pré-construída comentada. Retire o comentário dessa linha se quiser utilizar a imagem publicada com Compose.
Existe uma limitação importante em agosto de 2026: a imagem publicada é apenas linux/amd64. A parte arm64 da publicação multi-arquitetura falhou durante a emulação e aguarda runners arm nativos. Num VPS arm64, o pull falha com no matching manifest for linux/arm64/v8 in the manifest list entries. Nesse caso, a solução é criar a imagem a partir do clone.
Aponte para o seu servidor Jellyfin
Abra http://<host>:1420 e inicie sessão com o endereço, o nome de utilizador e a palavra-passe do seu servidor Jellyfin. O ficheiro .env.local.example no repositório destina-se apenas ao desenvolvimento local. O Vite expõe ao código do cliente as variáveis com o prefixo VITE_. Por isso, uma palavra-passe do Jellyfin escrita nesse ficheiro é compilada no bundle JavaScript que todos os visitantes descarregam. Num servidor acessível a outras pessoas, inicie sessão através da interface.
O navegador comunica diretamente com o Jellyfin. O contentor do Halcyon não faz proxy da API do Jellyfin. Há duas consequências que deve conhecer antes de começar a investigar problemas.
Primeiro, o Jellyfin tem de estar acessível a partir do navegador, e não apenas a partir do VPS que serve o Halcyon. Um Jellyfin associado a 127.0.0.1:8096 funciona num teste local, mas deixa as prateleiras vazias para todos os outros utilizadores.
Segundo, a chamada é cross-origin, do endereço do Halcyon para o endereço do Jellyfin. Por predefinição, o Jellyfin responde aos pedidos da API com Access-Control-Allow-Origin: *. Por isso, funciona sem configuração adicional. Se tiver restringido essa definição ou colocado um proxy de autenticação à frente da API do Jellyfin, a consola do navegador apresenta blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource e a aplicação carrega com as prateleiras vazias.
Coloque-o atrás de um reverse proxy, com autenticação à frente
vite preview é um servidor de pré-visualização. Não termina TLS (transport layer security) e não tem controlo de acesso próprio. Por isso, em qualquer exposição pública, deve ficar atrás do nginx ou do 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;
}
}Um nome de domínio à frente do contentor exige mais uma configuração. O Halcyon responde a localhost, a endereços IP diretos e aos nomes da máquina onde é executado, como proteção contra DNS rebinding. Dentro de um contentor, a máquina onde é executado é o próprio contentor. Por isso, o hostname não é o hostname que pretende usar. Um pedido recebido como halcyon.example.com é recusado, e a resposta indica o host recusado. Adicione esse 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-videoO valor é separado por vírgulas. Um ponto inicial, como .example.com, corresponde a subdomínios. all desativa a verificação. Use all apenas numa máquina que não possa ser acedida a partir do exterior.
Quando o store é servido através de https://, o endereço do Jellyfin introduzido no login também tem de ser https://. Um browser bloqueia uma chamada à API em http:// feita a partir de uma página HTTPS, e a consola apresenta Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource. O login falha sem qualquer explicação no Halcyon. Sirva ambos através de TLS ou mantenha ambos em HTTP simples numa rede privada.
Depois, configure a autenticação. O store pede as credenciais do Jellyfin, por isso um estranho que encontre o URL vê um ecrã de login. Uma funcionalidade altera esse comportamento. Ativar o Remote Play, em Settings e depois Connection, disponibiliza a sua sessão do Jellyfin ao servidor. Assim, os visitantes de /remote.html obtêm a sua própria instância da sua biblioteca real. Esse é o objetivo da funcionalidade. Isto significa que o sigilo do URL é a única barreira entre a Internet e os seus filmes. Se ativar o Remote Play, coloque single sign-on à frente de todo o site com Authentik como gateway SSO self-hosted ou remova o hostname público e aceda ao store através de um túnel WireGuard gerido com wg-easy.
Há dois detalhes relacionados. O reverse proxy transporta apenas o store. O stream do Remote Play usa WebRTC sobre UDP e não passa por um proxy HTTP. Por isso, precisa do seu próprio caminho em 3478/udp e em 49200 a 49260/udp quando o relay TURN incluído estiver a ser utilizado. Além disso, o docker run simples acima não mantém qualquer volume. Por isso, o seed do Remote Play não sobrevive a docker rm. O ficheiro Compose monta um volume halcyon-data em /data e define REMOTE_PLAY_SEED como /data/remote-play-seed.json precisamente por esse motivo.
O que fazer quando a loja funciona mal
O Halcyon renderiza sob demanda. Uma loja inativa não compõe frames, e perder o foco da janela interrompe o ciclo de animação. Por isso, deixar um separador aberto não esgota a bateria de um portátil. Isto ajuda quando a máquina está apenas no limite. Não resolve o problema de uma máquina que não consegue desenhar a loja.
Para esses clientes, existe um modo 2.5D. Ele usa apenas HTML e CSS, sem WebGL, e foi concebido para hardware tão limitado como um Raspberry Pi. Pode alternar entre 3D e 2.5D nas definições ou no menu de energia, sem recarregar a página. Assim, testar os dois modos no mesmo dispositivo demora apenas alguns segundos. Seja realista quanto ao resultado: o autor descreve o modo plano como rudimentar e ainda em desenvolvimento. Trate-o como uma alternativa para clientes fracos.
Quando um cliente é demasiado limitado para a loja 3D, a falha é evidente. O separador recarrega sozinho ou o navegador comunica que o contexto WebGL foi perdido, normalmente enquanto as prateleiras ainda estão a ser preenchidas. Mude esse dispositivo para 2.5D em vez de reduzir a sua biblioteca.
Fixe a imagem e verifique antes de fazer o pull
Leve esta parte a sério. As tags v0.1.0 a v0.3.1 foram publicadas com poucos dias de diferença, e v0.2.1 só existe porque o push da imagem para v0.2.0 falhou. Os relatórios de bugs são bem-vindos no projeto upstream, mas patches não são aceites, portanto a sequência de releases representa o estado de trabalho de uma única pessoa.
Executar latest com o hábito de docker pull significa que o store pode mudar num dia normal. Fixe a imagem pelo digest, a única referência que não pode mudar.
docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.3.1Esse comando mostra o digest associado à tag. Use-o no lugar da tag.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-video@sha256:747dcc821a3d2fa318b50e76024783c1835609047e84f502e23d021bc1898b20Esse digest era 0.3.1 em 10 August 2026. Consulte o valor atual, em vez de o copiar, e leia as notas da release antes de atualizar, porque uma patch release neste caso também pode alterar o layout do store, além de incluir correções.
FAQ
O Halcyon precisa de uma GPU na minha VPS?
Não para o uso normal. A loja é renderizada pelo three.js no navegador, por isso a máquina cliente faz o processamento e o contentor apenas serve ficheiros estáticos na porta 1420. A exceção é o Remote Play, que executa o Chromium headless no servidor e transmite o resultado. Esse caminho usa a CPU para renderizar, a menos que mapeie /dev/dri para o contentor para ativar a aceleração por hardware.
Posso colocar o Halcyon na Internet pública?
Apenas atrás de autenticação. A loja pede as credenciais do Jellyfin, mas ativar o Remote Play entrega a sessão do Jellyfin ao servidor. Assim, qualquer pessoa que aceda a /remote.html obtém uma instância da sua biblioteca real sem iniciar sessão. Coloque um reverse proxy com início de sessão único à frente do serviço ou mantenha o hostname fora do DNS público e aceda à loja através de uma VPN.
Por que motivo as prateleiras ficam vazias depois de iniciar sessão?
O navegador chama diretamente a API do Jellyfin. Por isso, o Jellyfin tem de estar acessível a partir do navegador, e não apenas a partir da VPS. Abra a consola do navegador. blocked by CORS policy significa que o Jellyfin não está a aceitar o pedido proveniente do endereço do Halcyon. Uma mensagem Mixed Content significa que a página está em HTTPS, enquanto o endereço do Jellyfin introduzido usa HTTP simples.
Preciso de --network host?
Apenas para o Remote Play. O WebRTC tem de anunciar o endereço real da máquina. Por trás da bridge Docker, o contentor só pode oferecer um endereço 172.x, que nenhum telefone da sua rede consegue alcançar. Para navegar na loja num navegador, -p 1420:1420 funciona e expõe muito menos do host.
Que tag de imagem devo usar?
Fixe um digest em vez de latest. Leia o digest de uma versão com docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.3.1, execute essa imagem pelo digest e só mude depois de ler as notas de versão. Em agosto de 2026, a imagem publicada é apenas linux/amd64. Por isso, um host arm64 tem de fazer o build a partir do clone com docker compose up -d.