Halcyon biến Jellyfin thành cửa hàng thuê băng đĩa 90s
Halcyon dựng lại thư viện Jellyfin thành cửa hàng thuê băng đĩa 1990s có thể đi lại bằng trình duyệt. Có Docker, reverse proxy và các giới hạn cần biết.
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 dưới dạng một cửa hàng băng đĩa video thập niên 1990 mà bạn có thể đi lại trong đó bằng trình duyệt. Mỗi bộ phim bạn sở hữu trở thành một vỏ hộp trên kệ. Bạn đi giữa các dãy kệ dưới đèn huỳnh quang, lấy một hộp xuống, lật mặt sau để đọc thông tin kỹ thuật, rồi mang đến quầy để bắt đầu phát. Thông tin về lần bắt đầu phát, 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 một Jellyfin server có sẵn qua Jellyfin API và không tự lưu thư viện riêng. Hướng dẫn này giả định Jellyfin đã chạy và quét thư viện không có lỗi. 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. Bạn cài công cụ này 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. 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 cố định version của image trước khi cho người khác sử dụng cửa hàng này. Phần cuối sẽ hướng dẫn cách thực hiện.
Render diễn ra ở đâu?
Trong browser. Halcyon là một app Vite và TypeScript xây dựng trên three.js, một thư viện JavaScript dùng WebGL để vẽ đồ họa 3D (web graphics library, giao diện của browser tới GPU). Máy đang xuất hình sẽ compositing geometry của store và box art.
Container hầu 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 serve các file đã build cùng một vài middleware route nhỏ. Halcyon không thực hiện 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ỏ có thể serve ứng dụng này mà không gặp vấn đề, vì việc serve chỉ gồm các file tĩnh và một vài route nhẹ qua HTTP. Laptop, tablet hoặc television đang 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. Private instance của Remote Play khởi chạy Chromium ở chế độ headless trên server, mỗi visitor chạy một instance, rồi stream store đã render tới phone hoặc set top box qua WebRTC (web real time communication). Luồng này render trên server, mặc định giới hạn ở 2 instance và có thể điều chỉnh bằng REMOTE_PLAY_MAX_INSTANCES. Server cũng cần GPU. Image được publish không kèm software renderer, nên nếu không có render node /dev/dri được map thì server sẽ từ chối khởi động private store và trả về This store server has no GPU available, so it can’t render a private store.
Thông tin store đọc từ thư viện của bạn
Các lối kệ lấy cấu trúc từ chính Jellyfin. Halcyon tạo các khu vực từ libraries và genres 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. Vì vậy, thông tin nào thiếu trong Jellyfin thì cũng thiếu trên kệ.
Điều này khiến 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à đã có sẵn artwork cùng genres sẽ hiển thị tốt hơn nhiều so với một thư mục chứa các file rời có tên chung chung. Photo libraries cũng phụ thuộc tương tự vào công cụ đã index chúng. Bạn nên lưu ý điều này khi cân nhắc PhotoPrism hay Immich cho các ảnh tĩnh lưu trên cùng server.
Dùng thử demo video store trước khi cài đặt
Dự án có bản demo chạy mà không cần server và không cần đăng nhập. Trong source hiện tại, bản được host này chứa các tựa streaming, không phải thư viện có cấu trúc giống thư viện của bạn, vì vậy hãy xem đây là cách tham quan store. Thêm ?demo=1 vào URL của deployment của bạn sẽ tải một thư viện tổng hợp thay thế.
Thư viện tổng hợp này dùng để kiểm tra phần cứng, nhưng có một điểm cần lưu ý: nó tự điều chỉnh theo thiết bị. Trình duyệt có GPU còn dư nhiều tài nguyên sẽ tải toàn bộ store với khoảng 2,000 tựa. GPU tầm trung sẽ tải khoảng 800 tựa. Điện thoại hoặc trình duyệt báo thiết bị có 4 GB memory trở xuống sẽ tải khoảng 300 tựa. Demo chạy mượt trên thiết bị yếu đã tự thu nhỏ quy mô, nên không cho biết nhiều về việc thư viện đầy đủ của bạn sẽ chạy thế nào trên thiết bị đó. Bài kiểm tra hữu ích là dùng thiết bị tải được toàn bộ store. Nếu giao diện bị giật, thư viện của bạn có quy mô tương tự cũng sẽ bị giật, và cách xử lý là dùng mode 2.5D được mô tả bên dưới thay vì 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-videoSau đó kiểm tra xem container đã khởi động chưa.
docker logs halcyon
curl -I http://127.0.0.1:1420Log 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 nghĩa là server không tự chuyển sang 1421 khi 1420 đã bị chiếm, nên dừng lại.
--network host dành cho Remote Play, không phải cho store. WebRTC phải thông báo địa chỉ thật của máy cho device muốn nhận stream. Khi chạy sau Docker bridge mặc định, container chỉ biết địa chỉ 172.x của chính nó. Không có điện thoại nào trong network của bạn truy cập được địa chỉ đó, nên stream không bao giờ kết nối. Nếu bạn chỉ muốn dùng store trong browser, hãy publish port thay thế.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-videoĐây là mặc định phù hợp hơn trên VPS, vì host networking đặt container trê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 giúp store tự khởi động lại sau reboot, theo cùng nguyên lý với Các service Compose tự khởi động khi boot.
Clone repository rồi chạy docker compose up -d sẽ build image locally thay thế. File Compose đã commit 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.
Published image hỗ trợ cả linux/amd64 và linux/arm64. Kiểm tra vào ngày 11 October 2026, bản release 0.25.0 hiện tại có digest riêng cho từng platform, nên VPS arm64 pull image bằng cùng command như mọi host khác. Giai đoạn chỉ hỗ trợ amd64 diễn ra trong thời gian ngắn: changelog đã bỏ image arm64 trong v0.2.1 ngày 8 August 2026 và đưa image này trở lại, được build trên native runner, trong v0.4.0 ngày 11 August 2026. Chỉ host bị pin vào tag trong khoảng thời gian đó mới phải build từ bản clone.
Trỏ ứng dụng đến server Jellyfin của bạn
Mở http://<host>:1420 và đăng nhập bằng địa chỉ server Jellyfin, username và password của bạn. File .env.local.example trong repository chỉ dùng cho môi trường development local. Vite đưa các biến có tiền tố VITE_ vào code phía client, vì vậy 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 chỉ phù hợp cho kiểm tra local và sẽ khiến shelves của mọi người khác 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, console của browser 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 lớp xác thực ở phía trước
vite preview là preview server. Nó không thực hiện TLS termination và không có access control riêng, nên trên môi trường public, nó phải nằm 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 đặt domain name phía trước container, bạn cần thêm một thiết lập. Halcyon phản hồi với localhost, raw IP address và tên của máy đang chạy nó để chống DNS rebinding. Bên 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 dưới dạng halcyon.example.com sẽ bị từ chối, và response sẽ nêu tên host 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-videoGiá 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ó nguồn nào bên ngoài truy cập được.
Sau khi store được phục vụ qua https://, địa chỉ Jellyfin bạn nhập khi đăng nhập cũng phải là https://. Trình duyệt sẽ chặn lời gọi API bằng http:// từ mộ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 sẽ thất bại mà không có giải thích nào bên trong Halcyon. Hãy phục vụ cả hai qua TLS, hoặc giữ cả hai dùng HTTP plain trong một private network.
Tiếp theo là xác thực. 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. Có một tính năng làm thay đổi điều này. Khi bật Remote Play trong Settings rồi đến Connection, bạn cấp session Jellyfin của mình cho server. Nhờ đó, visitor truy cập /remote.html sẽ nhận được instance riêng của thư viện thật của bạn. Đó chính là mục đích của tính năng này. Điều đó cũng có nghĩa là tính 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ó 2 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 path riêng trên 3478/udp và 49200 đến 49260/udp khi dùng TURN relay đi kèm. Ngoài ra, docker run plain ở trên không lưu volume nào, nên Remote Play seed sẽ không tồn tại sau docker rm. Compose file mount một volume halcyon-data tại /data và đặt REMOTE_PLAY_SEED thành /data/remote-play-seed.json vì đúng 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 composite frame nào, và khi mất focus cửa sổ, animation loop sẽ dừng. Vì vậy, một tab để mở không làm nóng pin laptop. Cơ chế này có ích với máy chỉ vừa đủ chạy. Nó không giúp được máy hoàn toàn không thể render store.
Với các client đó, Halcyon có chế độ 2.5D. Chế độ này chỉ dùng HTML và CSS, không dùng WebGL, và được thiết kế cho phần cứng yếu đến mức 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 đánh giá thực tế: vào tháng 8 năm 2026, tác giả gọi chế độ phẳng này là còn thô và cho biết project mới chỉ bắt đầu. Hãy xem đây là fallback cho các client yếu.
Khi một client không đủ tài nguyên để chạy store 3D, lỗi sẽ xuất hiện rõ ràng. 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 nội dung. 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 coi phần này là nghiêm túc. Các tag v0.1.0 đến v0.3.1 đều được phát hành cách nhau vài ngày, và v0.2.1 chỉ tồn tại vì lần push image cho v0.2.0 bị lỗi. Tốc độ này không chậm lại: v0.25.0 được phát hành vào 7 October 2026, 3 ngày sau v0.23.0. Bạn có thể gửi bug report lên upstream, nhưng không được gửi patch, nên chuỗi release này 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 bất kỳ lúc nào. Hãy pin bằng digest, vì đây là reference duy nhất không thể thay đổi.
docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0Lệnh này in ra digest đứng sau tag. Hãy dùng digest đó thay cho tag.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-video@sha256:7fc00e619a30004d275faa79ad36d810e5a8530757132603065ba4e848dd67deVào 11 October 2026, đó là digest đứng sau tag 0.25.0. 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 cập nhật, vì một patch release ở đây có thể bao gồm thay đổi layout của store cùng với các bản sửa lỗi.
FAQ
Halcyon có cần GPU trên VPS không?
Không, với nhu cầu sử dụng thông thường. Store được three.js render trong trình duyệt, nên máy client thực hiện việc render. Container chỉ phục vụ các file đã build và một vài route server nhẹ trên port 1420. Ngoại lệ là các private instance của Remote Play. Chúng chạy Chromium ở chế độ headless trên server và stream kết quả. Với image đã publish, luồng này cần map /dev/dri vào container. Nếu không có GPU, server sẽ từ chối khởi động private store.
Tôi có thể đưa Halcyon lên public Internet không?
Chỉ khi đặt sau lớp authentication. Store yêu cầu credential của Jellyfin, nhưng khi bật Remote Play, session Jellyfin của bạn được cung cấp cho server. Vì vậy, bất kỳ ai truy cập /remote.html đều có thể nhận một instance chứa thư viện thật 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 đưa hostname lên public DNS và truy cập store qua VPN.
Tại sao các kệ trống sau khi tôi đăng nhập?
Trình duyệt gọi trực tiếp Jellyfin API, nên Jellyfin phải có thể truy cập từ trình duyệt, không chỉ từ VPS. Mở browser console. blocked by CORS policy nghĩa là Jellyfin không chấp nhận request từ địa chỉ của Halcyon. Thông báo Mixed Content nghĩa là trang đ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ỉ cần cho Remote Play. WebRTC phải quảng bá địa chỉ thật của máy. Khi chạy sau Docker bridge, container chỉ có thể cung cấp địa chỉ 172.x mà không có điện thoại nào trong mạng của bạn truy cập được. Nếu chỉ duyệt store trong trình duyệt, -p 1420:1420 là đủ và làm lộ ít thông tin hơn nhiều về host.
Tôi 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.25.0, chạy bằng digest đó và chỉ nâng cấp sau khi đọc release notes. Tính đến ngày 11 October 2026, image đã publish có cả linux/amd64 và linux/arm64. Vì vậy, host arm64 có thể pull image như bình thường và không cần build từ clone.