SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-26

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

Halcyon을 사용하여 Jellyfin 라이브러리를 3D 비디오 대여점 환경으로 구현하는 방법을 알아봅니다. Docker 실행 명령어와 리버스 프록시 설정, 그리고 버전 고정이 필요한 이유 등 실제 운영 시 마주하게 되는 주의 사항을 상세히 정리했습니다.

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 관련 문제는 클라이언트의 영역입니다. 작은 VPS로도 충분히 서비스할 수 있는데, 이는 HTTP를 통해 정적 파일을 제공하는 것과 같기 때문입니다. 브라우저를 실행하는 노트북, 태블릿, TV가 스토어 화면을 부드럽게 움직일지 아니면 느리게 표시할지를 결정합니다.

단, 한 가지 기능은 이 규칙에서 예외입니다. Remote Play는 서버에서 헤드리스 Chromium 인스턴스를 생성하고, WebRTC(웹 실시간 통신)를 통해 렌더링된 스토어 화면을 휴대폰이나 셋톱박스로 스트리밍합니다. 이 경로는 서버에서 렌더링되며, 기본적으로 2개의 인스턴스로 제한되고 REMOTE_PLAY_MAX_INSTANCES를 통해 조정할 수 있습니다. 매핑된 /dev/dri 장치가 없으면 해당 인스턴스들은 CPU에서 렌더링되므로, 2코어 VPS는 추가 시청자가 늘어날 때마다 성능 저하를 체감하게 됩니다.

라이브러리에서 스토어가 읽어오는 정보

진열대는 Jellyfin의 자체 구조를 기반으로 합니다. Halcyon은 라이브러리와 장르를 기준으로 섹션을 구성하며, BoxSets에 포함된 속편들을 그룹화합니다. 각 케이스 뒷면에 인쇄된 사양은 Jellyfin이 이미 보유한 MediaStreams 메타데이터에서 가져옵니다. 따라서 Jellyfin에 누락된 정보는 선반에서도 나타나지 않습니다.

이로 인해 스토어는 사용자의 메타데이터를 그대로 반영하는 거울 역할을 합니다. Docker Compose의 arr 스택을 통해 아트워크와 장르 정보가 채워진 라이브러리는 일반적인 이름의 파일들만 모아둔 폴더보다 훨씬 보기 좋습니다. 사진 라이브러리 역시 인덱싱 도구에 동일하게 의존하므로, 같은 서버에 저장된 사진을 위해 PhotoPrism과 Immich를 비교할 때 이 점을 고려해야 합니다.

설치 전 비디오 스토어 데모 사용해 보기

이 프로젝트는 호스팅된 데모에서 합성 라이브러리를 대상으로 실행되는 전체 스토어를 공개하고 있습니다. 본인의 배포 환경에서도 Halcyon URL 뒤에 ?demo=1을 추가하면 동일한 기능을 사용할 수 있습니다.

이를 하드웨어 테스트 용도로 활용하십시오. 데모 라이브러리는 약 2,000개의 타이틀을 포함하며 브라우저 메모리를 대략 2 GB 정도 점유하는데, 이는 일반적인 개인 라이브러리보다 무거운 수준입니다. 만약 접속하려는 기기에서 데모가 끊긴다면 본인의 라이브러리도 끊길 가능성이 높습니다. 이 경우 더 큰 사양의 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 포트에서 대기 중이며, curlHTTP/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에서 게시된 이미지를 사용하려면 해당 라인의 주석을 해제하십시오.

2026년 8월 기준 한 가지 제약 사항이 있습니다. 게시된 이미지는 linux/amd64 전용입니다. 멀티 아키텍처 푸시의 arm64 부분은 에뮬레이션 환경에서 실패했으며 네이티브 arm 러너를 기다리고 있습니다. arm64 VPS에서 풀을 시도하면 no matching manifest for linux/arm64/v8 in the manifest list entries 오류가 발생하므로, 복제본에서 직접 빌드하는 것이 해결 방법입니다.

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(Transport Layer Security)를 종료하지 않으며 자체적인 접근 제어 기능도 없으므로, 공개된 환경에서는 반드시 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을 알아낸 외부인은 로그인 화면을 마주하게 됩니다. 이를 변경하는 기능이 하나 있습니다. 설정(Settings)의 연결(Connection) 메뉴에서 Remote Play를 활성화하면, 사용자의 Jellyfin 세션이 서버에 공유되어 /remote.html에 접속하는 방문자가 실제 라이브러리의 인스턴스를 직접 사용할 수 있게 됩니다. 이것이 해당 기능의 목적이며, 결과적으로 URL의 보안성이 인터넷과 사용자의 영상 파일 사이를 가로막는 유일한 방어선이 됩니다. Remote Play를 활성화한다면 Authentik을 자체 호스팅 SSO 게이트웨이로 사용하여 사이트 전체에 싱글 사인온을 적용하거나, 공개 호스트 이름을 제거하고 wg-easy로 관리되는 WireGuard 터널을 통해 스토어에 접근하십시오.

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

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

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

이러한 클라이언트를 위해 WebGL을 사용하지 않는 일반 HTML 및 CSS 기반의 2.5D 모드가 제공되며, 이는 Raspberry Pi와 같이 사양이 낮은 하드웨어를 대상으로 합니다. 설정이나 전원 메뉴에서 페이지를 새로 고침하지 않고도 3D와 2.5D 모드를 전환할 수 있으므로, 동일한 기기에서 두 모드를 테스트하는 데 몇 초밖에 걸리지 않습니다. 다만, 개발자가 이 평면 모드를 아직 거칠고 개발 중인 상태라고 설명했으므로 기대치를 현실적으로 조정해야 합니다. 이를 사양이 낮은 클라이언트를 위한 대체 수단으로 활용하십시오.

클라이언트 사양이 3D 스토어를 구동하기에 너무 낮으면 명확한 오류가 발생합니다. 보통 선반이 채워지는 도중에 탭이 스스로 새로 고침되거나 브라우저가 WebGL 컨텍스트 손실을 보고합니다. 이 경우 라이브러리를 축소하기보다는 해당 기기를 2.5D 모드로 전환하십시오.

이미지를 고정하고 풀(pull)하기 전에 확인하십시오

이 과정을 신중하게 수행하십시오. v0.1.0부터 v0.3.1까지의 태그는 모두 며칠 간격으로 배포되었으며, v0.2.1v0.2.0의 이미지 푸시가 실패했기 때문에 존재합니다. 업스트림에 버그 리포트를 보내는 것은 환영하지만 패치 제출은 허용되지 않으므로, 릴리스 스트림은 관리자 개인의 작업 상태를 반영합니다.

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

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

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

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

해당 다이제스트는 2026년 8월 10일에 0.3.1이었습니다. 값을 복사하지 말고 직접 현재 값을 확인하십시오. 또한 패치 릴리스에 수정 사항뿐만 아니라 저장소 레이아웃 변경이 포함될 수 있으므로, 버전을 이동하기 전에 릴리스 노트를 읽어보십시오.

FAQ

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

일반적인 사용 환경에서는 필요하지 않습니다. 스토어 화면은 브라우저에서 three.js를 통해 그려지므로 클라이언트 기기가 렌더링을 담당하며, 컨테이너는 포트 1420에서 정적 파일만 제공합니다. 예외적으로 Remote Play 기능은 서버에서 헤드리스 Chromium을 실행하여 결과를 스트리밍합니다. 이 경로는 하드웨어 가속을 위해 /dev/dri을 컨테이너에 매핑하지 않는 한 CPU에서 렌더링됩니다.

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

인증을 거친 경우에만 가능합니다. 스토어는 Jellyfin 자격 증명을 요구하지만, Remote Play를 활성화하면 사용자의 Jellyfin 세션이 서버로 전달됩니다. 따라서 /remote.html에 접속하는 사람은 누구나 별도의 로그인 없이 실제 라이브러리 인스턴스를 사용할 수 있게 됩니다. 반드시 SSO(Single Sign-On)가 적용된 리버스 프록시를 앞단에 배치하거나, 호스트 이름을 공용 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.3.1가 포함된 버전의 다이제스트를 확인하고 해당 다이제스트로 실행하며, 릴리스 노트를 읽은 후에만 업데이트하십시오. 2026년 8월 기준으로 게시된 이미지는 linux/amd64 전용이므로, arm64 호스트는 docker compose up -d을 사용하여 클론에서 직접 빌드해야 합니다.