SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

HalcyonでJellyfinを90年代のビデオ店にする方法

HalcyonはJellyfinのライブラリを、ブラウザーで歩ける1990年代のレンタル店に再現します。Dockerコマンド、リバースプロキシ、GPU負荷やバージョン固定の注意点を解説します。

Halcyon が Jellyfin ライブラリに行うこと

Halcyon Video は、ブラウザ上で Jellyfin ライブラリを歩いて回れる 1990 年代のビデオ店として再現します。所有している各映画は、棚に並ぶケースになります。蛍光灯の下で通路を歩き、ケースを棚から取り出し、裏返して背面の仕様を確認し、カウンターまで運んで再生を開始します。再生の開始、進行状況、停止は Jellyfin に返されるため、再開位置と視聴履歴は正しく保たれます。

Halcyon は Jellyfin API を介して既存の Jellyfin サーバーを読み取り、独自のライブラリは保持しません。このガイドでは、Jellyfin がすでに稼働し、問題なくスキャンできていることを前提とします。まだの場合は、まず VPS 上で Jellyfin をメディアサーバーとして構成する を参照し、通常の Web クライアントでライブラリが正しく表示される状態にしてから戻ってきてください。これは、すでにライブラリがあるから導入するものです。セルフホスト構成の一覧 に別のサービスが必要だから導入するものではありません。

このプロジェクトは GPL-3.0 で公開され、1 人で開発されています。README には、pull request を受け付けないことも明記されています。開発の進行が速く、回帰を検出する 2 人目のメンテナーもいないため、他の人にストアを公開する前にイメージのバージョンを固定してください。方法は最後のセクションで説明します。

レンダリングはどこで行われますか?

ブラウザー上です。Halcyon は three.js を基盤とする Vite および TypeScript アプリです。three.js は、WebGL(web graphics library。ブラウザーから GPU を利用するためのインターフェイス)を介して 3D グラフィックスを描画する JavaScript ライブラリです。店舗のジオメトリとボックスアートは、画面を表示するマシン上で合成されます。

コンテナが行う処理はごくわずかです。npm run serveを実行します。これは vite preview --port 1420 --strictPort --host です。さらに、ビルド済みファイルと少数の小さなミドルウェアルートを提供します。Halcyon はトランスコードを行わず、サーバー上でエンジンも実行しません。

したがって、GPU に関する問題はクライアント側の問題です。小規模な VPS でも問題なく提供できます。提供する処理は、HTTP 経由で静的ファイルと少数の軽量なルートを配信するだけだからです。ブラウザーを実行するラップトップ、タブレット、テレビによって、店舗が滑らかに動作するか、処理が遅くなるかが決まります。

この原則に反する機能が 1 つあります。Remote Play のプライベートインスタンスは、訪問者ごとにサーバー上でヘッドレス Chromium を 1 つ起動し、レンダリングした店舗を WebRTC(web real time communication)経由でスマートフォンまたはセットトップボックスにストリーミングします。この経路ではサーバー上でレンダリングが行われます。デフォルトでは 2 インスタンスに制限されており、REMOTE_PLAY_MAX_INSTANCES で変更できます。サーバー側にも GPU が必要です。公開済みのイメージにはソフトウェアレンダラーが含まれていません。そのため、/dev/dri のレンダリングノードが割り当てられていない場合、サーバーはプライベートストアの起動を拒否し、This store server has no GPU available, so it can’t render a private store. を返します。

ライブラリからストアが読み取る情報

通路の構成は Jellyfin 独自の構造に基づきます。Halcyon はライブラリとジャンルからセクションを配置し、BoxSets の情報を使って続編をグループ化します。各ケースの背面に表示される仕様は、Jellyfin がすでに保持している MediaStreams メタデータに基づきます。そのため、Jellyfin にない情報は棚にも表示されません。

つまり、このストアはメタデータを正確に反映します。アートワークとジャンルがすでに設定された Docker Compose の arr stack から供給されるライブラリは、一般的な名前のファイルだけをまとめたフォルダーよりも、ここで見栄えがよくなります。写真ライブラリも、インデックスを作成したソフトウェアに同じように依存します。同じサーバーに保存した写真について PhotoPrism と Immich の比較を検討するときは、この点を覚えておいてください。

インストール前にビデオストアのデモを試す

このプロジェクトには、サーバーもログインも不要で動作するデモがあります。現在のソースでは、ホストされているビルドの棚にはストリーミング用タイトルが並び、実際のライブラリと同じ構成ではありません。そのため、ストアの外観を確認するためのものとして扱ってください。独自のデプロイメントの URL に ?demo=1 を追加すると、代わりに合成ライブラリが読み込まれます。

この合成ライブラリはハードウェアのテストに使えますが、1 つ注意点があります。デバイスの性能に合わせてサイズが変わります。GPU に十分な余裕があるブラウザでは、約 2,000 タイトルの完全なストアが読み込まれます。中程度の GPU では約 800 タイトルです。スマートフォン、またはデバイスメモリが 4 GB 以下と報告されるブラウザでは、約 300 タイトルになります。性能の低いデバイスでデモが滑らかに動いても、ライブラリ自体が縮小されているため、完全なライブラリが同じデバイスで動作するかはほとんど判断できません。実用的なテストには、完全なストアが読み込まれるデバイスを使います。そこで表示が途切れる場合、同程度のサイズの独自ライブラリでも同じ問題が発生します。その場合は、より大きな VPS ではなく、下記で説明する 2.5D モードを使用してください。

Docker で実行する

これは upstream がドキュメントに記載しているコマンドです。

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

次に、起動したことを確認します。

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

ログには、preview server が port 1420 で待ち受けていることが表示され、curl は HTTP/1.1 200 OK に応答するはずです。数秒以内に終了するコンテナでは、ほぼ必ず port が原因です。--strictPort は、1420 が使用中でも server が 1421 へ切り替わらず、そのまま停止することを意味します。

--network host は Remote Play 用であり、store 用ではありません。WebRTC では、stream を要求する device に対して、マシンの実際の address を通知する必要があります。デフォルトの Docker bridge 配下では、container が認識できるのは自身の 172.x address だけです。ネットワーク上の phone からは到達できないため、stream は接続できません。browser で store を使用するだけなら、port を公開してください。

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

VPS では、これをデフォルトにする方が適しています。host networking により、container が public interface を含むマシン上のすべての interface を使用できるためです。VPS で Docker を実行する では、この選択の詳細を説明しています。--restart unless-stopped により reboot 後も store が起動します。これは boot 時に起動する Compose service と同じ考え方です。

repository を clone して docker compose up -d を実行すると、代わりに image がローカルで build されます。commit 済みの Compose file はデフォルトで source から build し、prebuilt image: の行をコメントアウトした状態で含んでいます。Compose で公開済みの image を使用する場合は、その行のコメントを解除してください。

公開済みの image は linux/amd64 と linux/arm64 の両方に対応しています。2026 年 10 月 11 日に確認した時点で、現在の 0.25.0 release には platform ごとに個別の digest が記載されています。そのため、arm64 VPS でも他の host と同じ command で pull できます。amd64 のみだった期間は短く、changelog では 2026 年 8 月 8 日の v0.2.1 で arm64 image が削除され、2026 年 8 月 11 日の v0.4.0 で native runner を使用して build された image が再び追加されています。その期間の tag に固定された host だけが、clone から build する必要があります。

Jellyfin サーバーを指定する

http://<host>:1420 を開き、Jellyfin サーバーのアドレス、ユーザー名、パスワードでログインします。リポジトリ内の .env.local.example ファイルはローカル開発専用です。Vite は VITE_ という接頭辞の付いた変数をクライアント側のコードに公開するため、そこに記述した Jellyfin のパスワードは、すべての訪問者がダウンロードする JavaScript バンドルに埋め込まれます。他のユーザーが到達できるサーバーでは、インターフェースからログインしてください。

ブラウザーは Jellyfin と直接通信します。Halcyon のコンテナは Jellyfin API をプロキシしません。デバッグを始める前に、次の2点を理解しておく必要があります。

1つ目は、Jellyfin が Halcyon を提供する VPS からだけでなく、ブラウザーからも到達可能でなければならないことです。127.0.0.1:8096 にバインドした Jellyfin はローカルテストには適していますが、それ以外のユーザーには棚が空のまま表示されます。

2つ目は、Halcyon のアドレスから Jellyfin のアドレスへ向かう、オリジン間の通信になることです。Jellyfin はデフォルトで API リクエストに Access-Control-Allow-Origin: * で応答するため、追加設定なしで動作します。この設定を制限している場合、または Jellyfin API の前段に認証プロキシを配置している場合、ブラウザーのコンソールに blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource と表示され、ストアは空の棚のまま読み込まれます。

リバースプロキシの背後に配置し、前段で認証する

vite preview はプレビューサーバーです。TLS(トランスポート層セキュリティ)の終端を行わず、独自のアクセス制御もありません。そのため、公開環境では nginx または 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;
  }
}

コンテナの前段にドメイン名を置く場合は、追加の設定が必要です。DNS リバインディング対策として、Halcyon は localhost、直接指定した IP アドレス、実行元のマシン名に応答します。コンテナ内では実行元のマシンがコンテナ自身になるため、ホスト名は実際のホスト名と異なります。halcyon.example.com として到着したリクエストは拒否され、拒否したホスト名が応答に示されます。その名前を追加してください。

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

値はコンマ区切りです。.example.com のように先頭にドットを付けると、サブドメインにも一致します。all を指定すると、このチェックが無効になります。all は、外部から到達できないマシンでのみ使用してください。

ストアを https:// で提供する場合、ログイン時に入力する Jellyfin のアドレスも https:// にする必要があります。HTTPS ページから実行した通常の http:// API 呼び出しはブラウザーにブロックされ、コンソールには Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource と表示されます。Halcyon 内に説明はなく、ログインは単に失敗します。両方を TLS 経由で提供するか、プライベートネットワーク内で両方を通常の HTTP のまま使用してください。

次に認証です。ストアは Jellyfin の認証情報を要求するため、URL を見つけた第三者にはログイン画面が表示されます。ただし、1 つの機能によって状況が変わります。Settings、Connection の順に開いて Remote Play を有効にすると、Jellyfin のセッションがサーバーに渡されます。その結果、/remote.html にアクセスした訪問者は、実際のライブラリの独自インスタンスを利用できます。これがこの機能の目的であり、インターネットと映画の間にあるのは URL の秘匿性だけになります。Remote Play を有効にする場合は、セルフホスト型 SSO ゲートウェイとしての Authentik を使ってサイト全体の前段にシングルサインオンを配置するか、公開ホスト名を廃止し、wg-easy で管理する WireGuard トンネル経由でストアにアクセスしてください。

関連する注意点が 2 つあります。リバースプロキシを通過するのはストアだけです。Remote Play のストリームは UDP 上の WebRTC であり、HTTP プロキシを通過しません。そのため、3478/udp と、組み込みの TURN リレーを使用する場合は 49200 から 49260/udp に、専用の経路が必要です。また、上記の通常の docker run はボリュームを保持しないため、Remote Play のシードは docker rm 後に失われます。Compose ファイルが halcyon-data ボリュームを /data にマウントし、まさにこの理由から REMOTE_PLAY_SEED を /data/remote-play-seed.json に設定しています。

ストアの動作が悪い場合の対処

Halcyon は必要なときに描画します。アイドル状態のストアはフレームを合成せず、ウィンドウのフォーカスが外れるとアニメーションループを停止します。そのため、タブを開いたままにしてもノート PC のバッテリーを過度に消費しません。これは性能がぎりぎりのマシンには有効です。ただし、ストアをまったく描画できないマシンには効果がありません。

そのようなクライアント向けに、WebGL を使わず、HTML と CSS だけで動作する 2.5D モードがあります。Raspberry Pi のような小型のハードウェアも対象です。設定または電源メニューから、ページを再読み込みせずに 3D と 2.5D を切り替えられます。同じデバイスで両方を試すのに数秒しかかかりません。ただし、期待値は現実的に設定してください。2026 年 8 月の時点で、作者はフラットモードを粗削りと説明し、プロジェクトは始まったばかりだと述べています。性能の低いクライアント向けのフォールバックとして扱ってください。

クライアントの性能が 3D ストアに足りない場合、障害は明確に現れます。通常、棚の読み込み中にタブが自動的に再読み込みされるか、ブラウザーが WebGL コンテキストの喪失を報告します。そのデバイスではライブラリを減らすのではなく、2.5D に切り替えてください。

イメージを固定し、pull 前に確認する

この点は軽視しないでください。v0.1.0 から v0.3.1 までのタグは数日の間にすべて公開され、v0.2.0 のイメージ push が失敗したためにのみ v0.2.1 が存在します。その後も公開ペースは落ちていません。v0.25.0 は 2026 年 10 月 7 日に公開され、v0.23.0 の 3 日後でした。上流ではバグ報告を受け付けていますが、patch は受け付けていません。そのため、リリースストリームは 1 人の作業中の状態です。

docker pull を習慣にして latest を実行すると、通常の火曜日にもストアの内容が変わる可能性があります。移動できない唯一の参照である digest で固定してください。

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

タグの背後にある digest が表示されます。タグの代わりにこの digest を使用してください。

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

2026 年 10 月 11 日時点では、これが 0.25.0 タグの背後にある digest でした。これをコピーせず、自分で現在の digest を確認してください。また、更新する前にリリースノートを確認してください。この場合、patch release には修正だけでなく、ストアのレイアウト変更が含まれることもあります。

FAQ

Halcyon の実行に VPS 上の GPU は必要ですか?

通常の利用では必要ありません。ストアはブラウザ上で three.js によって描画されるため、レンダリングはクライアントマシンが行います。コンテナはビルド済みファイルと、port 1420 で動作する軽量なサーバールートを提供するだけです。例外は Remote Play のプライベートインスタンスです。この機能はサーバー上でヘッドレス Chromium を実行し、結果をストリーミングします。公開イメージを使用する場合、この処理には /dev/dri のコンテナへのマッピングが必要です。GPU がない場合、サーバーはプライベートストアの起動を拒否します。

Halcyon をパブリックインターネット上に公開できますか?

認証の背後に置く場合に限り可能です。ストアは Jellyfin の認証情報を要求しますが、Remote Play を有効にすると Jellyfin のセッションがサーバーに渡されます。そのため、/remote.html にアクセスしたユーザーは、ログインせずに実際のライブラリのインスタンスを利用できます。シングルサインオンに対応したリバースプロキシを前段に置くか、ホスト名をパブリック DNS に登録せず、VPN 経由でストアにアクセスしてください。

ログイン後も棚が空なのはなぜですか?

ブラウザは Jellyfin API を直接呼び出すため、Jellyfin は VPS からだけでなく、ブラウザからも到達できる必要があります。ブラウザのコンソールを開いてください。blocked by CORS policy は、Jellyfin が Halcyon のアドレスからのリクエストを受け付けていないことを示します。Mixed Content メッセージは、ページが HTTPS で提供されている一方で、入力した Jellyfin のアドレスが通常の HTTP であることを示します。

--network host は必要ですか?

Remote Play の場合だけ必要です。WebRTC はマシンの実際のアドレスを通知する必要があります。Docker bridge の背後では、コンテナが提供できるのは 172.x アドレスだけです。このアドレスには、ネットワーク上のスマートフォンから到達できません。ブラウザでストアを閲覧するだけなら -p 1420:1420 で動作し、ホストの公開範囲も大幅に抑えられます。

どのイメージタグを使用すべきですか?

latest ではなく、digest を固定してください。docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0 を含むバージョンの digest を確認して、その digest を実行します。更新するのは、リリースノートを読んだ後にしてください。2026 年 10 月 11 日時点で、公開イメージには linux/amd64 と linux/arm64 の両方が含まれています。そのため、arm64 ホストでも通常どおり取得でき、clone からビルドする必要はありません。