SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor

Jellyfin을 90년대 비디오 대여점으로 만드는 Halcyon 설정법

Halcyon을 사용하여 Jellyfin 라이브러리를 3D 가상 공간으로 구현하는 방법을 안내합니다. Docker 설치 명령어와 리버스 프록시 설정, 그리고 1인 개발 프로젝트 특유의 버전 고정 및 성능 관련 주의사항을 상세히 정리했습니다.

Halcyon이 Jellyfin 라이브러리를 처리하는 방식

Halcyon Video는 사용자의 Jellyfin 라이브러리를 1990년대 비디오 대여점처럼 브라우저에서 걸어 다닐 수 있는 공간으로 재구성합니다. 소유한 모든 영화는 선반 위의 비디오 케이스로 변합니다. 사용자는 형광등 아래 통로를 걸어 다니며 상자를 꺼내고, 뒤집어서 뒷면의 사양을 읽고, 카운터로 가져가 재생을 시작할 수 있습니다. 재생 시작, 진행 상황, 정지 정보는 Jellyfin으로 다시 보고되므로 이어 보기 지점과 시청 기록이 정확하게 유지됩니다.

Halcyon은 Jellyfin API를 통해 기존 Jellyfin 서버를 읽어올 뿐 자체적인 라이브러리를 보관하지 않습니다. 이 가이드는 Jellyfin이 이미 실행 중이며 라이브러리 스캔이 정상적으로 완료된 상태를 전제로 합니다. 만약 그렇지 않다면 먼저 VPS에 미디어 서버로서의 Jellyfin을 설정하고, 일반 웹 클라이언트에서 라이브러리가 올바르게 표시되는지 확인한 뒤 돌아오십시오. 이 소프트웨어는 라이브러리가 이미 구축된 상태에서 설치하는 것이지, 셀프 호스팅 목록에 또 다른 서비스를 추가하기 위해 설치하는 것이 아닙니다.

이 프로젝트는 GPL-3.0 라이선스를 따르며 1인 개발자가 작성했습니다. README에는 풀 리퀘스트를 받지 않는다고 명시되어 있습니다. 개발 속도가 빠르고 회귀 버그를 잡아줄 다른 관리자가 없으므로, 다른 사람에게 이 비디오 대여점을 보여주기 전에 이미지 버전을 고정하십시오. 마지막 섹션에서 그 방법을 다룹니다.

렌더링은 어디에서 수행됩니까?

브라우저에서 수행됩니다. Halcyon은 Vite와 TypeScript로 작성된 앱이며, WebGL(브라우저의 GPU 인터페이스인 웹 그래픽 라이브러리)을 통해 3D 그래픽을 그리는 JavaScript 라이브러리인 three.js를 기반으로 합니다. 스토어 지오메트리와 박스 아트는 화면을 표시하는 기기에서 합성됩니다.

컨테이너가 수행하는 작업은 매우 적습니다. 컨테이너는 npm run serve를 실행하며, 이는 vite preview --port 1420 --strictPort --host입니다. 또한 빌드된 파일과 몇 가지 간단한 미들웨어 경로를 제공합니다. Halcyon은 서버에서 트랜스코딩을 수행하지 않으며 어떠한 엔진도 실행하지 않습니다.

따라서 GPU 관련 문제는 클라이언트의 몫입니다. 정적 파일과 몇 개의 가벼운 HTTP 경로만 처리하면 되므로 소규모 VPS에서도 원활하게 운영할 수 있습니다. 스토어가 부드럽게 움직일지 아니면 느리게 작동할지는 브라우저를 실행하는 노트북, 태블릿 또는 TV가 결정합니다.

단, 한 가지 기능은 이 규칙에서 예외입니다. Remote Play의 비공개 인스턴스는 서버에서 방문자당 하나씩 headless Chromium을 생성하며, 렌더링된 스토어를 WebRTC(웹 실시간 통신)를 통해 휴대폰이나 셋톱박스로 스트리밍합니다. 이 경로는 서버에서 렌더링을 수행하며, 기본적으로 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 스택을 통해 아트워크와 장르 정보가 채워진 라이브러리는 일반적인 파일명으로 구성된 폴더보다 훨씬 보기 좋습니다. 사진 라이브러리 역시 인덱싱을 수행한 도구에 의존하므로, 동일한 서버에 저장된 정지 영상 관리를 위해 PhotoPrism과 Immich를 비교할 때 이 점을 유념해야 합니다.

설치 전 비디오 스토어 데모 체험하기

이 프로젝트는 서버나 로그인 없이 실행되는 데모를 제공합니다. 현재 소스 코드에서 호스팅되는 빌드는 사용자의 라이브러리가 아닌 스트리밍 타이틀로 구성되어 있으므로, 스토어의 형태를 확인하는 용도로 활용하십시오. 자체 배포한 URL 뒤에 ?demo=1을 추가하면 합성 라이브러리가 로드됩니다.

이 합성 라이브러리는 하드웨어 테스트용이며, 한 가지 주의할 점은 장치 사양에 맞춰 크기가 조정된다는 것입니다. GPU 여유가 있는 브라우저에서는 약 2,000개의 타이틀로 구성된 전체 스토어가 로드됩니다. 중간급 GPU에서는 약 800개가 로드됩니다. 스마트폰이나 장치 메모리가 4 GB 이하인 브라우저에서는 약 300개가 로드됩니다. 사양이 낮은 장치에서 원활하게 실행되는 데모는 이미 크기가 축소된 상태이므로, 실제 전체 라이브러리가 해당 장치에서 어떻게 작동할지 판단하기에는 정보가 부족합니다. 유용한 테스트는 전체 스토어가 로드되는 장치에서 수행하는 것입니다. 만약 이때 화면이 끊긴다면 비슷한 규모의 사용자 라이브러리도 끊길 가능성이 높습니다. 이 경우 더 큰 VPS를 사용하는 것보다 아래에 설명된 2.5D 모드를 적용하는 것이 해결책입니다.

Docker로 실행하기

업스트림 문서에서 권장하는 명령어는 다음과 같습니다.

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

로그를 보면 프리뷰 서버가 1420 포트에서 대기 중이며, curl이 HTTP/1.1 200 OK에 응답해야 합니다. 컨테이너가 몇 초 만에 종료된다면 거의 항상 포트 문제 때문입니다. --strictPort은 1420 포트가 이미 사용 중일 때 서버가 1421 포트로 이동하지 못하고 그대로 중단됨을 의미합니다.

--network host는 스토어가 아닌 Remote Play를 위한 것입니다. WebRTC는 스트리밍을 요청하는 기기에 해당 머신의 실제 주소를 알려주어야 합니다. 기본 Docker 브리지 환경에서 컨테이너는 자신의 172.x 주소만 알고 있는데, 이는 네트워크상의 어떤 휴대폰에서도 접근할 수 없으므로 스트림이 연결되지 않습니다. 브라우저에서 스토어만 이용하려면 대신 포트를 퍼블리시하십시오.

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

VPS에서는 이것이 더 나은 기본 설정입니다. 호스트 네트워킹을 사용하면 컨테이너가 퍼블릭 인터페이스를 포함하여 머신이 가진 모든 인터페이스에 노출되기 때문입니다. VPS에서 Docker 실행하기에서 이와 관련된 나머지 내용을 다룹니다. --restart unless-stopped은 재부팅 후 스토어를 다시 실행하는 방법으로, 부팅 시 시작되는 Compose 서비스와 같은 원리입니다.

저장소를 복제하고 docker compose up -d을 실행하면 이미지를 로컬에서 빌드합니다. 커밋된 Compose 파일은 기본적으로 소스에서 빌드하도록 설정되어 있으며, 미리 빌드된 image: 라인은 주석 처리되어 있습니다. Compose에서 게시된 이미지를 사용하려면 해당 라인의 주석을 해제하십시오.

게시된 이미지는 linux/amd64와 linux/arm64을 모두 포함합니다. 2026년 10월 11일 확인 결과, 현재 0.25.0 릴리스는 플랫폼별로 별도의 다이제스트를 나열하므로 arm64 VPS도 다른 호스트와 동일한 명령어로 풀(pull)할 수 있습니다. amd64 전용이었던 기간은 짧았습니다. 변경 로그에 따르면 2026년 8월 8일 v0.2.1에서 arm64 이미지가 제외되었다가, 2026년 8월 11일 v0.4.0에서 네이티브 러너로 빌드되어 다시 추가되었습니다. 해당 기간의 태그로 고정된 호스트만 복제본에서 직접 빌드하면 됩니다.

Jellyfin 서버를 가리키도록 설정

http://<host>:1420를 열고 Jellyfin 서버 주소, 사용자 이름, 비밀번호로 로그인합니다. 저장소에 있는 .env.local.example 파일은 로컬 개발용으로만 사용해야 합니다. Vite는 VITE_ 접두사가 붙은 변수를 클라이언트 측 코드로 노출하므로, 여기에 Jellyfin 비밀번호를 작성하면 모든 방문자가 다운로드하는 JavaScript 번들에 비밀번호가 포함됩니다. 외부에서 접근 가능한 서버라면 인터페이스를 통해 직접 로그인하십시오.

브라우저는 Jellyfin과 직접 통신합니다. Halcyon 컨테이너는 Jellyfin API를 프록시하지 않으며, 디버깅을 시작하기 전에 알아두어야 할 두 가지 결과가 있습니다.

첫째, Jellyfin은 Halcyon을 서비스하는 VPS뿐만 아니라 브라우저에서도 접근 가능해야 합니다. 127.0.0.1:8096에 바인딩된 Jellyfin은 로컬 테스트에는 적합하지만, 다른 사용자에게는 빈 화면만 표시됩니다.

둘째, 호출은 Halcyon 주소에서 Jellyfin 주소로 향하는 교차 출처(cross origin) 요청입니다. Jellyfin은 기본적으로 Access-Control-Allow-Origin: *로 API 요청에 응답하므로 별도의 설정 없이도 작동합니다. 만약 해당 설정을 제한했거나 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;
  }
}

컨테이너 앞단에 도메인 이름을 설정하려면 추가적인 설정이 필요합니다. Halcyon은 DNS 리바인딩 공격을 방지하기 위해 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을 알아낸 외부인은 로그인 화면을 마주하게 됩니다. 이 동작을 변경하는 기능이 하나 있습니다. 설정의 연결(Connection) 메뉴에서 원격 재생(Remote Play)을 켜면, 사용자의 Jellyfin 세션이 서버에 공유되어 /remote.html에 접속하는 방문자가 실제 라이브러리의 개별 인스턴스를 사용할 수 있게 됩니다. 이것이 해당 기능의 목적이며, 따라서 URL의 비밀 유지 여부가 인터넷과 사용자의 영화 데이터 사이를 가로막는 유일한 방어선이 됩니다. 원격 재생을 활성화할 경우, Authentik을 자체 호스팅 SSO 게이트웨이로 사용하여 사이트 전체에 싱글 사인온을 적용하거나, 공개 호스트 이름을 제거하고 wg-easy로 관리되는 WireGuard 터널을 통해 스토어에 접근하십시오.

이와 관련하여 두 가지 세부 사항이 있습니다. 리버스 프록시는 스토어만 전달합니다. 원격 재생 스트림은 UDP 기반의 WebRTC를 사용하므로 HTTP 프록시를 통과하지 않습니다. 따라서 3478/udp 포트와, 번들된 TURN 릴레이를 사용할 경우 49200에서 49260/udp 포트에 대한 별도의 경로가 필요합니다. 또한 위에서 언급한 일반 docker run은 볼륨을 유지하지 않으므로, 원격 재생 시드 데이터는 docker rm 이후에 사라집니다. Compose 파일이 /data에 halcyon-data 볼륨을 마운트하고 REMOTE_PLAY_SEED를 /data/remote-play-seed.json으로 설정하는 이유가 바로 이것입니다.

스토어 성능이 저하될 때의 대처 방법

Halcyon은 요청 시에만 렌더링을 수행합니다. 유휴 상태의 스토어는 프레임을 합성하지 않으며, 창의 포커스를 잃으면 애니메이션 루프가 중단됩니다. 이것이 탭을 열어두어도 노트북 배터리가 소모되지 않는 이유입니다. 이러한 방식은 성능이 경계선에 있는 기기에는 도움이 되지만, 스토어를 전혀 그릴 수 없는 기기에는 아무런 효과가 없습니다.

이러한 클라이언트를 위해 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.1은 v0.2.0의 이미지 푸시가 실패했기 때문에 존재합니다. 그 이후로도 속도는 줄어들지 않았습니다. v0.25.0은 v0.23.0가 배포된 지 3일 후인 2026년 10월 7일에 게시되었습니다. 업스트림에 버그 리포트를 보내는 것은 환영하지만 패치 제출은 그렇지 않으므로, 이 릴리스 스트림은 한 사람의 작업 상태를 그대로 반영합니다.

docker pull하는 습관을 가지고 latest을 실행하면 평범한 화요일에도 저장소의 내용이 예고 없이 바뀔 수 있습니다. 변경될 수 없는 유일한 참조인 다이제스트(digest)로 고정하십시오.

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

이 명령은 태그 뒤에 숨겨진 다이제스트를 출력합니다. 태그 대신 이 값을 사용하십시오.

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 태그 뒤에 있는 다이제스트였습니다. 값을 복사하지 말고 직접 현재 값을 읽으십시오. 또한 이동하기 전에 릴리스 노트를 읽으십시오. 이곳의 패치 릴리스에는 수정 사항뿐만 아니라 저장소 레이아웃 변경 사항도 포함될 수 있기 때문입니다.

FAQ

Halcyon을 VPS에서 실행할 때 GPU가 필요한가요?

일반적인 사용 시에는 필요하지 않습니다. 스토어는 브라우저에서 three.js를 통해 렌더링되므로 클라이언트 기기가 렌더링을 수행하며, 컨테이너는 빌드된 파일과 포트 1420의 가벼운 서버 라우트만 제공합니다. 예외적으로 Remote Play의 프라이빗 인스턴스는 서버에서 헤드리스 Chromium을 실행하고 결과를 스트리밍합니다. 배포된 이미지의 경우, 해당 경로에 /dev/dri이 컨테이너 내부로 매핑되어야 합니다. GPU가 없으면 서버는 프라이빗 스토어 시작을 거부합니다.

Halcyon을 공용 인터넷에 공개해도 되나요?

인증을 거친 경우에만 가능합니다. 스토어는 Jellyfin 자격 증명을 요구하지만, Remote Play를 활성화하면 사용자의 Jellyfin 세션이 서버로 전달됩니다. 따라서 /remote.html에 접속하는 사람은 누구나 로그인 없이 사용자의 실제 라이브러리 인스턴스를 이용할 수 있습니다. 리버스 프록시를 앞에 두고 싱글 사인온(SSO)을 설정하거나, 호스트 이름을 공용 DNS에 등록하지 말고 VPN을 통해서만 스토어에 접근하십시오.

로그인 후 선반이 비어 있는 이유는 무엇인가요?

브라우저가 Jellyfin API를 직접 호출하므로, Jellyfin은 VPS뿐만 아니라 브라우저에서도 접근 가능해야 합니다. 브라우저 콘솔을 확인하십시오. blocked by CORS policy는 Jellyfin이 Halcyon 주소로부터의 요청을 수락하지 않음을 의미합니다. Mixed Content 메시지는 페이지는 HTTPS인데 입력한 Jellyfin 주소는 일반 HTTP임을 나타냅니다.

--network host이 필요한가요?

Remote Play를 사용할 때만 필요합니다. WebRTC는 기기의 실제 주소를 알려야 하는데, Docker 브리지 뒤에 있는 컨테이너는 네트워크상의 휴대폰이 접근할 수 없는 172.x 주소만 제공할 수 있기 때문입니다. 브라우저에서 스토어를 탐색할 때는 -p 1420:1420로 충분하며, 호스트의 정보를 훨씬 적게 노출합니다.

어떤 이미지 태그를 사용해야 하나요?

latest 대신 다이제스트(digest)를 고정하십시오. docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0이 포함된 버전의 다이제스트를 확인하고 해당 다이제스트로 실행하며, 릴리스 노트를 읽은 후에만 업데이트하십시오. 2026년 10월 11일 기준으로 배포된 이미지에는 linux/amd64와 linux/arm64이 모두 포함되어 있으므로, arm64 호스트는 다른 이미지와 마찬가지로 이를 가져와 사용할 수 있으며 클론으로부터 직접 빌드할 필요가 없습니다.