SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-22

Halcyon biến Jellyfin thành cửa hàng video thập niên 90

Halcyon biến thư viện Jellyfin thành cửa hàng cho thuê video ảo thập niên 1990. Xem lệnh Docker, reverse proxy và các giới hạn cần biết trước khi dùng.

Halcyon làm gì với thư viện Jellyfin của bạn

Halcyon Video hiển thị lại thư viện Jellyfin của bạn dưới dạng một cửa hàng video ảo kiểu thập niên 1990 trong trình duyệt. Mỗi bộ phim bạn sở hữu trở thành một hộp băng trên kệ. Bạn đi giữa các dãy kệ dưới ánh đèn dài, lấy một hộp xuống, lật mặt sau để đọc thông số, rồi mang đến quầy để bắt đầu phát. Thông tin bắt đầu, tiến độ và dừng phát được gửi lại cho Jellyfin, nên vị trí tiếp tục xem và lịch sử xem vẫn chính xác.

Halcyon đọc server Jellyfin hiện có thông qua Jellyfin API và không tự duy trì thư viện riêng. Hướng dẫn này giả định Jellyfin đã chạy và quét thư viện ổn định. Nếu chưa, trước tiên hãy thiết lập Jellyfin làm media server trên VPS, rồi quay lại khi thư viện hiển thị đúng trong web client thông thường. Đây là loại ứng dụng bạn cài vì thư viện đã có sẵn, không phải vì cần thêm một service vào danh sách self-hosting của mình.

Dự án dùng giấy phép GPL-3.0 và do một người viết, đồng thời README nêu rõ dự án không nhận pull request. Quá trình phát triển diễn ra nhanh và không có maintainer thứ hai để phát hiện regression, vì vậy hãy pin version của image trước khi cho người khác xem cửa hàng. Phần cuối sẽ hướng dẫn cách thực hiện việc đó.

Render diễn ra ở đâu?

Trong browser. Halcyon là một app Vite và TypeScript được xây dựng trên three.js, một thư viện JavaScript vẽ đồ họa 3D thông qua WebGL (web graphics library, giao diện của browser với GPU). Máy đang kết nối với màn hình sẽ compositing hình học của cửa hàng và box art.

Container gần như không làm gì nhiều. Nó chạy npm run serve, tức là vite preview --port 1420 --strictPort --host, rồi phục vụ các file đã build cùng một vài middleware route nhỏ. Halcyon không thêm bước transcoding và không chạy engine trên server.

Vì vậy, câu hỏi về GPU thuộc về client. Một VPS nhỏ vẫn phục vụ tốt, vì công việc này chỉ là phục vụ static file qua HTTP. Laptop, tablet hoặc television chạy browser mới quyết định store chuyển động mượt hay bị giật.

Có một tính năng không theo quy tắc này. Remote Play tạo các instance Chromium headless trên server rồi stream store đã render đến phone hoặc set top box qua WebRTC (web real time communication). Với luồng này, việc render diễn ra trên server. Mặc định hệ thống giới hạn ở hai instance và có thể điều chỉnh bằng REMOTE_PLAY_MAX_INSTANCES. Nếu không map thiết bị /dev/dri, các instance đó sẽ render bằng CPU, nên một VPS 2 core sẽ nhanh chóng bị ảnh hưởng khi có thêm người xem.

Thông tin store đọc từ thư viện của bạn

Các dãy kệ lấy cấu trúc từ chính Jellyfin. Halcyon tạo các khu vực từ library và genre của bạn, đồng thời nhóm các phần tiếp theo từ BoxSets. Thông số in ở mặt sau mỗi vỏ đĩa lấy từ metadata MediaStreams mà Jellyfin đã lưu, nên thông tin nào thiếu trong Jellyfin thì cũng thiếu trên kệ.

Vì vậy, store phản ánh khá chính xác metadata của bạn. Một library được cung cấp bởi một arr stack trong Docker Compose với artwork và genre đã được điền sẵn sẽ trông tốt hơn nhiều ở đây so với một thư mục chứa các file rời có tên chung chung. Photo library cũng phụ thuộc tương tự vào hệ thống đã index chúng. Bạn nên nhớ điều này khi cân nhắc PhotoPrism và Immich cho các ảnh tĩnh nằm trên cùng server.

Thử demo cửa hàng video trước khi cài đặt

Dự án cung cấp toàn bộ cửa hàng chạy với một thư viện tổng hợp tại demo được host. Thêm ?demo=1 vào bất kỳ URL Halcyon nào sẽ cho kết quả tương tự trên deployment của bạn.

Hãy dùng demo để kiểm tra phần cứng. Thư viện demo có khoảng 2,000 tựa đề và cần khoảng 2 GB bộ nhớ trình duyệt, nặng hơn hầu hết thư viện cá nhân. Nếu demo bị giật trên thiết bị bạn dự định dùng để duyệt, thư viện của bạn cũng sẽ bị giật. Cách khắc phục là dùng chế độ 2.5D được mô tả bên dưới, không phải nâng cấp VPS.

Chạy bằng Docker

Đây là command được upstream hướng dẫn.

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

Sau đó kiểm tra container đã khởi động chưa.

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

Log phải cho thấy preview server đang listen trên port 1420 và curl phải trả lời HTTP/1.1 200 OK. Container thoát sau vài giây thì gần như luôn là do port. --strictPort có nghĩa là server không tự chuyển sang 1421 khi 1420 đã bị chiếm, nên server dừng lại.

--network host dành cho Remote Play, không phải cho store. WebRTC phải quảng bá địa chỉ thực của máy cho thiết bị muốn nhận stream. Khi dùng Docker bridge mặc định, container chỉ biết địa chỉ 172.x của chính nó. Điện thoại trong mạng của bạn không thể truy cập địa chỉ này, nên stream không bao giờ kết nối được. Nếu bạn chỉ muốn dùng store trong browser, hãy publish port thay vì dùng cách này.

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

Đây là lựa chọn mặc định tốt hơn trên VPS, vì host networking đưa container lên mọi interface của máy, bao gồm cả interface public. Chạy Docker trên VPS trình bày phần còn lại của đánh đổi này. --restart unless-stopped là tùy chọn giúp store khởi động lại sau reboot, giống với Các Compose service khởi động cùng hệ thống.

Clone repository rồi chạy docker compose up -d sẽ build image locally thay vì dùng image có sẵn. File Compose trong repository mặc định build từ source và có dòng image: dùng image dựng sẵn đang được comment. Bỏ comment dòng đó nếu bạn muốn dùng published image trong Compose.

Một giới hạn quan trọng tính đến tháng 08 năm 2026: published image chỉ có linux/amd64. Phần arm64 của lần push multi-architecture thất bại khi chạy qua emulation và đang chờ native arm runner. Trên VPS arm64, lệnh pull sẽ fail với no matching manifest for linux/arm64/v8 in the manifest list entries. Khi đó, build từ bản clone là cách xử lý.

Trỏ đến Jellyfin server của bạn

Mở http://<host>:1420 và đăng nhập bằng địa chỉ Jellyfin server, username và password của bạn. File .env.local.example trong repository chỉ dùng cho môi trường phát triển local. Vite đưa các biến có tiền tố VITE_ vào client-side code, nên password Jellyfin ghi trong đó sẽ được biên dịch vào JavaScript bundle mà mọi visitor tải xuống. Trên server mà người khác có thể truy cập, hãy đăng nhập qua giao diện.

Browser giao tiếp trực tiếp với Jellyfin. Container của Halcyon không proxy Jellyfin API. Điều này dẫn đến 2 điểm cần biết trước khi bắt đầu debug.

Thứ nhất, browser phải truy cập được Jellyfin, không chỉ VPS đang phục vụ Halcyon. Jellyfin bind vào 127.0.0.1:8096 phù hợp cho test local, nhưng sẽ khiến shelves của mọi người khác bị trống.

Thứ hai, request là cross-origin, từ địa chỉ của Halcyon đến địa chỉ của Jellyfin. Mặc định, Jellyfin trả lời API request bằng Access-Control-Allow-Origin: *, nên không cần cấu hình thêm. Nếu bạn đã thu hẹp setting đó hoặc đặt authentication proxy phía trước Jellyfin API, browser console sẽ báo blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource và store sẽ tải lên với các shelves trống.

Đặt phía sau reverse proxy và thêm authentication ở phía trước

vite preview là preview server. Nó không thực hiện TLS termination (transport layer security) và không có access control riêng, nên mọi instance public phải đặt phía sau nginx hoặc 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;
  }
}

Khi dùng domain name phía trước container, cần thêm một thiết lập. Halcyon chấp nhận localhost, địa chỉ IP trực tiếp và tên của máy đang chạy nó để chống DNS rebinding. Trong container, máy đang chạy nó chính là container, nên hostname của nó không phải hostname của bạn. Request đến với tên halcyon.example.com sẽ bị từ chối và response cho biết host nào đã bị từ chối. Hãy thêm tên đó.

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

Giá trị được phân tách bằng dấu phẩy. Dấu chấm ở đầu, chẳng hạn .example.com, sẽ khớp với các subdomain. all sẽ tắt kiểm tra này. Chỉ dùng all trên máy mà không có gì bên ngoài có thể truy cập.

Khi store được phục vụ qua https://, địa chỉ Jellyfin nhập khi đăng nhập cũng phải là https://. Browser sẽ chặn một API call dạng plain http:// được gọi từ trang HTTPS, còn console sẽ hiển thị Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource. Đăng nhập chỉ thất bại mà Halcyon không hiển thị lý do. Hãy phục vụ cả hai qua TLS, hoặc giữ cả hai ở plain HTTP trong một private network.

Tiếp theo là authentication. Store yêu cầu thông tin đăng nhập Jellyfin, nên người lạ tìm thấy URL sẽ gặp màn hình đăng nhập. Một feature sẽ thay đổi điều này. Bật Remote Play trong Settings rồi đến Connection sẽ đưa Jellyfin session của bạn cho server, để visitor truy cập /remote.html nhận được instance riêng của thư viện thật của bạn. Đó là mục đích của feature này, đồng thời có nghĩa là độ bí mật của URL là ranh giới duy nhất giữa Internet và các bộ phim của bạn. Nếu bật Remote Play, hãy đặt single sign-on phía trước toàn bộ site bằng Authentik làm SSO gateway tự host, hoặc bỏ public hostname và truy cập store qua WireGuard tunnel được quản lý bằng wg-easy.

Có hai chi tiết liên quan. Reverse proxy chỉ chuyển tiếp store. Stream Remote Play là WebRTC qua UDP và không đi qua HTTP proxy, nên cần một đường đi riêng trên 3478/udp và 49200 đến 49260/udp khi dùng TURN relay đi kèm. Ngoài ra, docker run dạng plain ở trên không lưu volume, nên seed của Remote Play sẽ không tồn tại sau docker rm. File Compose mount một halcyon-data volume tại /data và đặt REMOTE_PLAY_SEED thành /data/remote-play-seed.json chính vì lý do đó.

Cách xử lý khi store chạy không ổn định

Halcyon render theo nhu cầu. Store ở trạng thái idle không dựng frame nào. Khi mất focus cửa sổ, vòng lặp animation cũng dừng. Vì vậy, một tab mở sẵn không làm pin laptop nhanh hết. Cách này giúp ích cho máy chỉ vừa đủ khả năng chạy. Nó không giải quyết được trường hợp máy hoàn toàn không thể render store.

Với những client đó, có chế độ 2.5D. Chế độ này chỉ dùng HTML và CSS, không dùng WebGL, và được thiết kế cho cả phần cứng yếu như Raspberry Pi. Bạn có thể chuyển giữa 3D và 2.5D trong settings hoặc power menu mà không cần reload trang. Vì vậy, việc thử cả hai chế độ trên cùng một thiết bị chỉ mất vài giây. Hãy đặt kỳ vọng thực tế: tác giả mô tả chế độ phẳng này là còn thô và vẫn đang được phát triển. Hãy xem đây là fallback cho các client yếu.

Khi client không đủ khả năng chạy store 3D, lỗi thường rất rõ. Tab tự reload, hoặc browser báo WebGL context bị mất, thường xảy ra khi các kệ vẫn đang được tải. Hãy chuyển thiết bị đó sang 2.5D thay vì cắt bớt library.

Ghim image và kiểm tra trước khi pull

Hãy xem phần này là nghiêm túc. Các tag từ v0.1.0 đến v0.3.1 được phát hành chỉ cách nhau vài ngày, còn v0.2.1 tồn tại chỉ vì thao tác push image cho v0.2.0 đã thất bại. Bạn có thể gửi bug report lên upstream, nhưng không được gửi patch, vì vậy chuỗi release chỉ phản ánh trạng thái làm việc của một người.

Chạy latest với thói quen docker pull có nghĩa là store có thể thay đổi vào bất kỳ ngày thứ Ba bình thường nào. Hãy pin theo digest, là reference duy nhất không thể thay đổi.

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

Lệnh này in ra digest đứng sau tag. Dùng digest đó thay cho tag.

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

Digest đó là 0.3.1 vào ngày 10 tháng 8 năm 2026. Hãy tự đọc digest hiện tại thay vì sao chép giá trị này, đồng thời đọc release notes trước khi chuyển phiên bản, vì một patch release ở đây có thể bao gồm cả thay đổi layout của store lẫn các bản sửa lỗi.

FAQ

Halcyon có cần GPU trên VPS không?

Không, với cách dùng thông thường. Store được three.js render trong browser, nên máy client thực hiện việc render còn container chỉ phục vụ các static file trên port 1420. Ngoại lệ là Remote Play, tính năng chạy Chromium ở chế độ headless trên server rồi stream kết quả. Luồng này render bằng CPU, trừ khi bạn map /dev/dri vào container để bật hardware acceleration.

Tôi có thể đưa Halcyon lên public Internet không?

Chỉ khi đặt authentication ở phía trước. Store yêu cầu credentials của Jellyfin, nhưng khi bật Remote Play, session Jellyfin của bạn được gửi cho server. Vì vậy, bất kỳ ai truy cập /remote.html cũng có thể dùng một instance chứa library thực của bạn mà không cần đăng nhập. Hãy đặt một reverse proxy có single sign-on ở phía trước, hoặc không công khai hostname trên public DNS và truy cập store qua VPN.

Tại sao các shelf trống sau khi tôi đăng nhập?

Browser gọi trực tiếp Jellyfin API, nên Jellyfin phải có thể truy cập từ browser, không chỉ từ VPS. Mở console của browser. blocked by CORS policy có nghĩa là Jellyfin không chấp nhận request từ địa chỉ của Halcyon. Thông báo Mixed Content có nghĩa là page đang dùng HTTPS, còn địa chỉ Jellyfin bạn nhập là HTTP thuần.

Tôi có cần --network host không?

Chỉ khi dùng Remote Play. WebRTC phải quảng bá địa chỉ thật của máy. Khi ở sau Docker bridge, container chỉ có thể cung cấp địa chỉ 172.x mà không phone nào trong network của bạn có thể truy cập. Nếu chỉ browse store bằng browser, -p 1420:1420 là đủ và làm lộ host ít hơn nhiều.

Nên dùng image tag nào?

Hãy pin digest thay vì latest. Đọc digest của một version có docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.3.1, chạy bằng digest đó, và chỉ chuyển version sau khi đọc release notes. Tính đến August 2026, image được publish chỉ là linux/amd64, nên host arm64 phải build từ clone bằng docker compose up -d.