SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor

Halcyon: Turn Jellyfin to 90s Video Store

Make your Jellyfin library feel like 1990s rental store for browser. Get the Docker command, reverse proxy setup, and honest caveats before you deploy am.

Wetin Halcyon dey do for your Jellyfin library

Halcyon Video dey redraw your Jellyfin library as video store for 1990s wey person fit waka inside through browser. Every film wey you get go become case for shelf. You go waka between aisles under strip lights, pull box down, turn am over to read the specs for the back, then carry am go counter to start playback. Playback reports start, progress, and stop back to Jellyfin, so resume points and watch history go remain correct.

Halcyon dey read existing Jellyfin server through Jellyfin API and e no keep any library of e own. This guide assume say Jellyfin don already dey run and e dey scan without error. If e no be so, first set up Jellyfin as media server for VPS and come back when your library dey look correct for the normal web client. Na the kind thing wey you install because the library don already dey there, no be because you need another service for your self-hosting list.

The project na GPL-3.0 and one person write am, and README talk clearly say e no dey accept pull requests. Development dey move fast, and no second maintainer dey to catch regression, so pin the image version before you show the store to anybody else. Last section explain how.

Render na where?

Na inside browser. Halcyon na Vite and TypeScript app wey build on three.js, a JavaScript library wey dey draw 3D graphics through WebGL (web graphics library, browser interface to GPU). The machine wey hold the screen dey combine the store geometry and the box art.

The container no dey do much. E dey run npm run serve, wey be vite preview --port 1420 --strictPort --host, and e dey serve the built files plus some small middleware routes. Halcyon no add transcoding and e no run any engine for server.

So na client get the GPU question. Small VPS fit serve this well, because serving am mean static files over HTTP. Na laptop, tablet, or television wey dey run browser go decide whether the store go move smoothly or crawl.

One feature dey break this rule. Remote Play dey start headless Chromium instances for server and stream the rendered store go phone or set top box through WebRTC (web real time communication). For this path, rendering dey happen for server. By default, e get limit of two instances, and you fit adjust am with REMOTE_PLAY_MAX_INSTANCES. If no mapped /dev/dri device dey available, the instances go render for CPU. So two core VPS go feel every extra viewer.

Wetin store dey read from your library

The aisles come from Jellyfin own structure. Halcyon dey arrange sections from your libraries and genres, and e dey group sequels from your BoxSets. The specs wey dey printed for the back of each case come from the MediaStreams metadata wey Jellyfin already hold. So anything wey no dey for Jellyfin no go dey for the shelf.

This one make the store be fair mirror of your metadata. Library wey an arr stack for Docker Compose feed, with artwork and genres already filled, go look much better here pass folder wey get loose files and generic names. Photo libraries get the same dependency on wetin index dem. Remember this when you dey compare PhotoPrism with Immich for the still images wey dey the same server.

Try the video store demo before you install anything

Project dey publish the complete store wey dey run with synthetic library for the hosted demo. If you append ?demo=1 to any Halcyon URL, e go do the same thing for your own deployment.

Use am to test hardware, but make you note one thing: demo dey adjust im size based on the device. Browser wey get enough GPU capacity go load the complete store of around 2,000 titles. Browser with average GPU go get around 800. Phone, or any browser wey report 4 GB device memory or less, go get around 300. If demo dey run smoothly for weak device, e don already reduce im size by itself, so e no go tell you much about how your complete library go run there. The useful test na device wey fit load the complete store. If e dey stutter, your own library wey get similar size go stutter too. The fix na the 2.5D mode wey we describe below, no be bigger VPS.

Run am with Docker

Na be the command upstream documentation provide.

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

Then check whether e don start.

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

The log suppose show say preview server dey listen on port 1420, and curl suppose answer HTTP/1.1 200 OK. If container stop within some seconds, na almost always port problem. --strictPort mean say server no go shift go 1421 when 1420 don already dey use, so e stop instead.

--network host na for Remote Play, no be for the store. WebRTC must advertise the machine real address to the device wey wan receive the stream. Behind the default Docker bridge, container know only im own 172.x address. No phone for your network fit reach that address, so stream no go connect. If na only store for browser you want, publish the port instead.

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

This one na better default for VPS, because host networking put the container on every interface wey the machine get, including the public one. Running Docker for VPS explain the remaining trade-off. --restart unless-stopped na wetin bring the store back after reboot, with the same idea as Compose services wey start on boot.

If you clone the repository and run docker compose up -d, e go build the image locally instead. The committed Compose file dey build from source by default, and e get the prebuilt image: line commented out. Uncomment that line if you want use the published image with Compose.

The published image support both linux/amd64 and linux/arm64. As checked on 9 October 2026, the current 0.25.0 release list separate digest for each platform, so arm64 VPS go pull am with the same command as any other host. The amd64-only period short: changelog remove the arm64 image for v0.2.1 on 8 August 2026 and bring am back, built on native runners, for v0.4.0 on 11 August 2026. Na only host wey pin to tag from that period need build from the clone.

Point am to your Jellyfin server

Open http://<host>:1420 and log in with your Jellyfin server address, username and password. The .env.local.example file for the repository na only for local development. Vite dey expose variables wey start with VITE_ to client-side code, so if you write Jellyfin password there, dem go compile am inside the JavaScript bundle wey every visitor dey download. For server wey other people fit reach, use the interface to log in.

Browser dey talk to Jellyfin directly. Halcyon's container no dey proxy Jellyfin API, and this get two consequences wey you need know before you start debugging.

First, browser need fit reach Jellyfin, no be only the VPS wey dey serve Halcyon. Jellyfin wey bind to 127.0.0.1:8096 dey okay for local test, but e go leave the shelves empty for everybody else.

Second, the call na cross-origin call, from Halcyon's address go Jellyfin address. Jellyfin dey answer API requests with Access-Control-Allow-Origin: * by default, so e go work without extra configuration. If you don narrow that setting, or you put authentication proxy for front of Jellyfin API, browser console go report blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource and store go load with empty shelves.

Put am behind reverse proxy, with authentication for front

vite preview na preview server. E no terminate TLS (transport layer security), and e no get access control of im own. So, e suppose dey behind nginx or Caddy for anything wey public.

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

Domain name wey dey front of the container need one extra setting. Halcyon dey answer to localhost, raw IP addresses, and the names of the machine wey e dey run on, to protect against DNS rebinding. Inside container, the machine wey e dey run on na the container, so the hostname no be your own. Request wey arrive as halcyon.example.com go dey refused, and the response go name the host wey e refuse. Add that name.

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

The value dey comma separated. Leading dot like .example.com go match subdomains, while all go turn off the check. Use all only for machine wey nothing outside fit reach.

Once the store dey served over https://, the Jellyfin address wey you type for login must be https:// too. Browser go block plain http:// API call wey HTTPS page make, and the console go read Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource. Login go simply fail, with no explanation inside Halcyon. Serve both over TLS, or keep both on plain HTTP inside private network.

Then authentication. The store dey ask for Jellyfin credentials, so stranger wey find the URL go meet login screen. One feature dey change this. If you turn on Remote Play under Settings, then Connection, e go give your Jellyfin session to the server. Visitors wey go /remote.html go then get their own instance of your real library. Na the feature purpose be this, and e mean say na secrecy of the URL dey between internet and your films. If you enable Remote Play, put single sign on in front of the whole site with Authentik as self-hosted SSO gateway, or remove the public hostname and reach the store through WireGuard tunnel wey wg-easy manage.

Two details follow this setup. Reverse proxy dey carry only the store. Remote Play stream na WebRTC over UDP, and e no dey pass through HTTP proxy. So e need im own path on 3478/udp and 49200 to 49260/udp when the bundled TURN relay dey in use. Also, the plain docker run above no keep any volume, so Remote Play seed no go survive docker rm. The Compose file mount a halcyon-data volume at /data and set REMOTE_PLAY_SEED to /data/remote-play-seed.json for exactly this reason.

Wetin to do when store dey run badly

Halcyon dey render only when e need am. Store wey dey idle no dey composite any frame, and once window lose focus, animation loop dey stop. Na why tab wey you leave open no dey drain laptop battery. This fit help machine wey performance just dey borderline. E no go help machine wey no fit draw the store at all.

For those clients, 2.5D mode dey available. E use plain HTML and CSS without WebGL, and dem design am for hardware as small as Raspberry Pi. You fit switch between 3D and 2.5D from settings or power menu without page reload. So you fit test both modes for the same device within seconds. Make you get realistic expectation: the author describe flat mode as rough, and development still dey continue. Use am as fallback for weak clients.

When client too weak for 3D store, failure go show clearly. The tab fit reload by itself, or browser fit report lost WebGL context, usually while shelves still dey fill. Move that device to 2.5D instead of reducing your library.

Pin the image, and check before you pull

Take this part seriously. Tags v0.1.0 through v0.3.1 all land within days of each other, and v0.2.1 dey exist only because image push for v0.2.0 fail. The pace no slow down since then: v0.25.0 publish on 7 October 2026, three days after v0.23.0. You fit send bug reports upstream, but patches no dey accepted, so the release stream na one person’s working state.

If you dey run latest with habit of docker pull, the store fit change under you on any normal Tuesday. Pin am by digest, na the only reference wey no fit move.

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

This go print the digest behind the tag. Use am instead of the tag.

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

That digest be 0.25.0 on 9 October 2026. Read the current one yourself instead of copying am. Read the release notes before you move, because patch release for here fit carry store layout changes together with fixes.

FAQ

Halcyon need GPU for my VPS?

No be for normal use. three.js dey draw the store inside browser, so na client machine dey do the rendering, while container only dey serve static files for port 1420. The exception na Remote Play, wey dey run headless Chromium for server and stream the result. That path dey render with CPU unless you map /dev/dri into the container for hardware acceleration.

I fit put Halcyon for public internet?

Only if authentication dey front am. The store dey ask for Jellyfin credentials, but when you turn on Remote Play, your Jellyfin session dey go the server. So anybody wey load /remote.html fit get instance of your real library without login. Put reverse proxy with single sign on in front of am, or keep the hostname out of public DNS and reach the store through VPN.

Why shelves empty after I log in?

The browser dey call Jellyfin API directly, so Jellyfin must dey reachable from the browser, no be only from VPS. Open browser console. blocked by CORS policy mean say Jellyfin no dey accept request from Halcyon's address. Mixed Content message mean say page dey use HTTPS, while the Jellyfin address wey you enter na plain HTTP.

I need --network host?

Na only for Remote Play. WebRTC must advertise the machine real address. Behind Docker bridge, the container fit offer only 172.x address wey no phone for your network fit reach. To browse the store inside browser, -p 1420:1420 dey work and e expose much less of the host.

Which image tag I suppose use?

Pin digest instead of latest. Read the digest for version wey get docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0, run that digest, and move only after you read the release notes. As of 9 October 2026, the published image get both linux/amd64 and linux/arm64. So arm64 host go pull am like any other image and e no need build from the clone.