Docker Compose로 Prowlarr, Sonarr, Radarr 구축
Prowlarr, Sonarr, Radarr, qBittorrent를 하나의 Docker Compose 파일로 구성합니다. 하드링크가 깨지지 않는 볼륨 레이아웃과 PUID, PGID 설정으로 효율적인 미디어 서버를 구축하는 방법을 설명합니다.
구축할 내용
Docker Compose 기반의 arr 스택은 미디어 라이브러리를 관리하는 4개의 컨테이너로 구성됩니다. 인덱서 설정을 담당하는 Prowlarr, TV 시리즈를 위한 Sonarr, 영화를 위한 Radarr, 그리고 다운로드 클라이언트인 qBittorrent가 포함됩니다. 이들은 Compose 네트워크상에서 서비스 이름으로 서로 통신하며, 호스트의 단일 폴더 트리를 공유합니다. 설치 과정은 간단합니다. 이 스택이 수년간 안정적으로 작동할지, 아니면 매주 문제를 일으킬지를 결정하는 핵심은 볼륨 레이아웃이므로, 이 가이드의 대부분은 이를 다룹니다.
이 스택이 콘텐츠를 직접 찾아주지는 않습니다. Prowlarr는 사용자가 추가한 인덱서를 보관하며, 어떤 인덱서를 사용할지는 사용자의 결정이자 법적 책임입니다. 이 가이드는 사용자, 경로, 권한, 컨테이너 네트워킹, 그리고 정상 작동 여부를 확인하는 점검 방법 등 기반 설정을 다룹니다.
Compose 파일을 작성해 본 적이 없다면 먼저 VPS를 위한 Docker Compose 기초를 읽어보십시오. 이 게시물은 서버에서 docker compose version 명령이 이미 정상적으로 실행되는 환경을 가정합니다.
하드링크가 깨지는 이유와 그것이 중요한 이유
Sonarr가 다운로드를 완료하면 파일을 라이브러리로 가져옵니다. 다운로드 폴더와 라이브러리 폴더가 동일한 파일 시스템에 있으면, 가져오기 작업은 하드링크로 수행됩니다. 하드링크는 디스크상의 동일한 데이터를 가리키는 두 번째 이름일 뿐입니다. 추가 공간을 차지하지 않으며 시간도 걸리지 않습니다. 미디어 서버가 새 이름을 읽는 동안 토렌트는 이전 이름으로 계속 시딩(seeding)됩니다.
두 폴더가 서로 다른 파일 시스템에 있으면 커널은 해당 링크를 생성할 수 없습니다. 이 경우 Sonarr는 복사 방식으로 전환합니다. 40 GB 분량의 시즌 파일은 이제 80 GB의 디스크 공간을 차지하며, 입출력 작업으로 인해 수 분의 시간이 소요됩니다. 가져오기 로그에는 하드링크가 실패하여 파일이 복사되었다는 기록이 남습니다. 고정된 디스크 용량을 사용하는 VPS 환경에서는 이러한 방식으로 일주일 만에 디스크 공간이 부족해지곤 합니다.
여기서 함정이 발생합니다. 컨테이너 내부에서 바인드 마운트(bind mount)는 파일 시스템의 경계가 됩니다. /mnt/data/torrents를 /downloads으로, /mnt/data/media를 /tv로 마운트하면, 두 경로가 호스트 디스크의 같은 위치에 있더라도 Sonarr는 이를 두 개의 분리된 마운트로 인식하여 링크 생성을 거부합니다. 공식 LinuxServer.io 이미지 문서에서도 이를 명시하고 있습니다. 즉, 별도의 /downloads 및 /tv 경로를 사용하면 하드링크 기능을 사용할 수 없습니다.
해결책은 단일 마운트를 사용하는 것입니다. 미디어를 다루는 모든 컨테이너에 동일한 하나의 볼륨인 /mnt/data:/data을 할당하고, 사용하는 모든 경로는 그 내부의 폴더로 설정하십시오. 하나의 마운트 지점과 하나의 파일 시스템을 사용해야 하드링크가 정상적으로 작동합니다.
사용자, 그룹 및 폴더 생성
컨테이너는 PUID 및 PGID에 설정된 숫자 형태의 사용자 ID로 파일을 작성합니다. sudo 없이 SSH를 통해 파일을 읽고 수정할 수 있도록 본인의 계정을 사용하십시오.
id -u
id -g두 명령어 모두 일반적인 Ubuntu VPS에서는 1000를 출력합니다. 이제 디렉터리 구조를 생성하십시오. 미디어가 저장된 디스크에 이 구조를 배치하고, 전체 트리를 해당 디스크 하나에 유지하십시오.
sudo mkdir -p /mnt/data/torrents/movies /mnt/data/torrents/tv
sudo mkdir -p /mnt/data/media/Movies /mnt/data/media/Shows
sudo chown -R 1000:1000 /mnt/data
sudo chmod -R 775 /mnt/data진행하기 전에 해당 경로가 동일한 파일 시스템인지 확인하십시오.
df --output=source,target /mnt/data/torrents /mnt/data/media두 줄 모두 동일한 소스 장치를 표시해야 합니다. 서로 다른 장치라면 컨테이너 설정에서 무엇을 지정하든 하드링크는 작동하지 않습니다.
라이브러리 폴더는 의도적으로 Movies 및 Shows로 명명했습니다. 이미 미디어 서버로 Jellyfin을 운영 중이라면, /mnt/data/media를 Jellyfin에 /media으로 마운트하십시오. 그러면 해당 라이브러리가 가이드에 명시된 위치인 /media/Movies 및 /media/Shows에 생성됩니다.
환경 파일
서버마다 값이 달라지는 설정은 .env에 작성하여 Compose 파일 옆에 둡니다.
mkdir -p ~/arr && cd ~/arr~/arr/.env을 다음과 같이 작성합니다:
PUID=1000
PGID=1000
TZ=Etc/UTC
DATA_ROOT=/mnt/dataTZ을 Europe/Berlin와 같이 본인의 시간대로 설정합니다. arr 애플리케이션은 해당 시간대를 기준으로 작업을 예약하고 로그를 기록하므로, 잘못된 값을 설정하면 나중에 모든 로그를 해석하기 어려워집니다.
Compose 파일
~/arr/docker-compose.yml를 작성합니다:
services:
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
volumes:
- ./config/prowlarr:/config
ports:
- 127.0.0.1:9696:9696
restart: unless-stopped
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
volumes:
- ./config/sonarr:/config
- ${DATA_ROOT}:/data
ports:
- 127.0.0.1:8989:8989
restart: unless-stopped
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
volumes:
- ./config/radarr:/config
- ${DATA_ROOT}:/data
ports:
- 127.0.0.1:7878:7878
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
- WEBUI_PORT=8080
- TORRENTING_PORT=6881
volumes:
- ./config/qbittorrent:/config
- ${DATA_ROOT}:/data
ports:
- 127.0.0.1:8080:8080
- 6881:6881
- 6881:6881/udp
stop_grace_period: "10s"
restart: unless-stopped이 파일에서 실질적인 작업을 수행하는 항목은 네 가지입니다.
${DATA_ROOT}:/data는 미디어에 접근하는 세 개의 컨테이너에서 동일하게 설정됩니다. Prowlarr는 미디어 파일을 열지 않으므로 이 설정이 포함되지 않습니다.
모든 웹 포트는 127.0.0.1에 바인딩되어 있으므로, Docker는 이를 루프백 주소로만 게시합니다. 단순히 8989:8989을 사용하면 모든 인터페이스에 포트가 게시되며, Docker 자체 방화벽 규칙이 ufw deny 규칙을 우회하여 트래픽을 허용하게 됩니다. 이러한 동작은 사용자들을 자주 당황하게 만들며, 이에 대한 설명은 Docker가 ufw를 우회하여 포트를 게시하는 이유에서 확인할 수 있습니다.
포트 6881은 의도적으로 모든 인터페이스에 게시됩니다. 이는 토렌트 수신 대기 포트이며, 들어오는 피어 연결을 위해 접근 가능해야 하기 때문입니다. sudo ufw allow 6881을 사용하여 해당 포트를 허용하십시오. 이 명령어가 생소하다면 VPS를 위한 ufw 방화벽 기초를 읽어보시기 바랍니다.
설정 디렉터리는 애플리케이션별로 분리되어 있으며, 미디어 볼륨만 공유됩니다. 첫 실행 전에 디렉터리를 생성하여 root가 아닌 사용자 계정의 소유로 설정하십시오:
mkdir -p ~/arr/config/prowlarr ~/arr/config/sonarr ~/arr/config/radarr ~/arr/config/qbittorrent
docker compose up -d
docker compose ps네 가지 서비스 모두 running를 읽어야 합니다. 2026년 7월 기준으로 이 이미지들은 lscr.io에 게시되며, latest 태그는 현재 안정화 버전을 따릅니다. 업데이트를 예기치 않은 상황이 아닌 의도적인 결정으로 관리하려면 특정 버전 태그를 고정하십시오.
웹 인터페이스에 안전하게 접근하기
포트가 루프백 주소에 바인딩되어 있으므로 아직 외부로 노출되지 않았습니다. 자신의 컴퓨터에서 SSH를 통해 포트를 포워딩하십시오:
ssh -L 9696:127.0.0.1:9696 -L 8989:127.0.0.1:8989 \
-L 7878:127.0.0.1:7878 -L 8080:127.0.0.1:8080 you@your-server이제 브라우저에서 http://127.0.0.1:8989 주소로 서버의 Sonarr에 접근할 수 있습니다. 영구적인 접근을 원한다면 여러 애플리케이션을 위한 Traefik과 TLS 인증서 뒤에 스택을 배치하거나, 직접 호스팅하는 WireGuard VPN을 통해 서버에 접속하십시오. 이러한 애플리케이션 중 어느 것도 자체 로그인 페이지만을 방어 수단으로 삼아 공용 인터넷에 직접 노출해서는 안 됩니다. 리버스 프록시 방식을 선택하고 네 개의 애플리케이션 로그인을 각각 관리하는 대신 하나의 계정으로 통합하고 싶다면, Authentik을 사용하여 자체 호스팅 SSO를 구축할 수 있으며, Traefik의 forward auth 기능을 통해 모든 요청에 대해 인증을 강제할 수 있습니다.
qBittorrent는 최초 실행 시 무작위 관리자 비밀번호를 생성하여 컨테이너 로그에 출력합니다. 로그를 확인한 뒤 웹 인터페이스에서 비밀번호를 변경하십시오:
docker compose logs qbittorrent | grep -i password비밀번호를 변경하지 않으면 재시작할 때마다 새로운 무작위 비밀번호가 생성되므로, 매번 로그를 다시 확인해야 합니다.
각 애플리케이션 내 경로 설정
qBittorrent에서 Options를 열고 Downloads로 이동한 뒤, 기본 저장 경로를 /data/torrents으로 설정합니다. 완료되지 않은 다운로드 폴더도 /data/torrents/incomplete와 같이 동일한 트리 내에 유지하십시오. /data 외부에서 완료된 다운로드는 라이브러리로 하드링크할 수 없습니다.
Sonarr에서 Settings를 열고 Media Management로 이동한 뒤, 루트 폴더 /data/media/Shows을 추가합니다. Radarr의 루트 폴더는 /data/media/Movies입니다. 이는 컨테이너 내부의 경로입니다. 호스트 경로인 /mnt/data/media/Shows은 컨테이너 관점에서 해당 디렉터리가 존재하지 않으므로 거부됩니다.
Sonarr와 Radarr 모두에서 Settings를 열고 Download Clients로 이동한 뒤, qBittorrent를 추가합니다. 호스트는 qbittorrent이고 포트는 8080입니다. Compose가 모든 4개의 컨테이너를 내부 DNS(domain name system) 서비스가 포함된 하나의 네트워크에 배치하므로 서비스 이름이 호스트 이름으로 작동합니다. 여기에서 localhost을 사용하지 마십시오. Sonarr 컨테이너 내부에서 localhost는 Sonarr 자신을 의미합니다.
Remote Path Mappings는 비워 두십시오. 이 기능은 다운로드 클라이언트가 보고하는 경로를 arr 애플리케이션이 인식할 수 있는 경로로 변환하기 위해 존재합니다. 하나의 공유 /data 마운트를 사용하면 두 컨테이너가 모든 경로를 이미 동일하게 인식하게 되며, 이것이 이 구성을 사용하는 두 번째 이유입니다.
Prowlarr를 Sonarr 및 Radarr에 연결하기
Prowlarr는 인덱서 정의를 다른 애플리케이션으로 전달하므로, 인덱서를 두 번 설정할 필요 없이 한 번만 구성하면 됩니다. 이를 위해서는 각 애플리케이션의 API(Application Programming Interface) 키가 필요합니다.
Sonarr에서 Settings를 열고 General로 이동한 뒤 API 키를 복사합니다. Prowlarr에서 Settings를 열고 Apps로 이동하여 Sonarr 애플리케이션을 추가한 다음 세 가지 필드를 입력합니다. Prowlarr Server는 http://prowlarr:9696입니다. Sonarr Server는 http://sonarr:8989입니다. API Key에는 복사한 값을 입력합니다. Test를 누릅니다. 녹색 결과가 나오면 Prowlarr가 Compose 네트워크를 통해 Sonarr에 성공적으로 도달한 것입니다. Radarr도 http://radarr:7878에서 동일하게 반복합니다.
연결이 거부되었다는 빨간색 결과는 거의 항상 서비스 이름이 잘못되었거나 http:// 접두사가 누락되었음을 의미합니다. 컨테이너 내부에서 이름이 올바르게 해석되는지 확인하십시오.
docker compose exec prowlarr curl -sS -o /dev/null -w '%{http_code}\n' http://sonarr:8989HTTP 상태 코드가 출력되면 네트워크 경로가 정상임을 의미합니다. 이름 해석 오류가 발생하면 서비스 이름이 잘못된 것입니다.
하드링크가 실제로 작동하는지 확인하기
링크 카운트를 확인하기 전까지는 설정을 신뢰해서는 안 됩니다. 항목 하나를 가져온(import) 후, 다운로드된 파일과 라이브러리 파일을 비교하십시오:
stat -c '%i %h %n' /mnt/data/torrents/tv/*/*.mkv
stat -c '%i %h %n' /mnt/data/media/Shows/*/*/*.mkv첫 번째 숫자는 inode이며 두 번째는 링크 카운트입니다. 하드링크된 파일은 두 위치에서 동일한 inode를 나타내며 링크 카운트는 2이 됩니다. 서로 다른 inode가 각각 1의 링크 카운트를 가진다면, 이는 Sonarr가 파일을 복사했음을 의미하며 가져오기 로그에 하드링크가 실패했다는 메시지가 기록됩니다.
디스크 사용량도 확인하십시오. 하드링크는 데이터 복사 없이 이름만 추가하는 방식이므로, 가져오기가 발생할 때 df -h /mnt/data은 거의 변하지 않아야 합니다.
실제로 발생하는 문제
가져오기 과정에서 발생하는 권한 오류는 컨테이너의 사용자 ID가 라이브러리 폴더에 쓰기 권한을 가지고 있지 않음을 의미합니다. 관련 메시지는 Access to the path ... is denied입니다. ls -ln /mnt/data/media를 사용하여 소유자 ID가 PUID과 일치하는지 확인하십시오. 또한 컨테이너가 디렉터리에 진입하려면 해당 디렉터리에 실행(execute) 비트가 설정되어 있어야 한다는 점을 기억하십시오.
파일 소유자가 root로 표시되는 것은 호스트 디렉터리가 존재하기 전에 컨테이너가 먼저 시작되어 Docker가 해당 디렉터리를 root 권한으로 생성했기 때문입니다. 스택을 중지하고 디렉터리를 chown한 뒤 다시 시작하십시오.
qBittorrent에서 토렌트를 삭제했을 때 라이브러리 파일까지 사라진다면, 가져오기 방식이 복사(copy)로 설정되어 나중에 삭제되었거나 토렌트 항목이 아닌 실제 데이터를 삭제한 경우입니다. 올바르게 하드링크(hardlink)를 사용했다면, 하나의 이름을 삭제해도 다른 이름은 그대로 유지됩니다. 데이터는 링크 카운트가 0이 될 때만 해제되기 때문입니다.
추가한 미디어 용량보다 디스크가 더 빠르게 차오르는 현상은 복사(copy) 문제의 가장 비용이 많이 드는 형태입니다. 저장 공간을 추가로 구매하기 전에 위에서 언급한 stat 확인 절차를 먼저 수행하십시오.
이 스택을 운영하기 위한 VPS 요구 사항
세 가지 arr 애플리케이션은 가볍다. 인덱서에 폴링하고, 작은 SQLite 데이터베이스에 기록하며, 파일 이름을 변경한다. RAM이 2 GB인 서버에서도 4개 컨테이너를 모두 무리 없이 실행할 수 있다. 부하는 다른 작업에서 발생한다. 대용량 토렌트를 처리할 때 다운로드 클라이언트가 디스크 입출력을 포화시키고, 같은 장비에서 미디어 서버로 동영상을 트랜스코딩하면 CPU를 사용한다. 실제 처리량이 충분한 볼륨에 미디어를 저장하고, 서버에서 다른 중요한 작업도 수행한다면 다운로드 클라이언트에 대역폭 제한을 설정한다. 여유 용량이 있다고 가정하지 말고 다른 작업에 필요한 자원을 별도로 산정한다. 자체 호스팅 AFFiNE 워크스페이스는 데이터베이스를 포함한 또 다른 4개 컨테이너로 구성되며, 2 GB 장비에서는 메모리 대부분을 자체적으로 사용하려 한다. 모든 추가 서비스에 그만큼의 자원이 필요한 것은 아니다. 자체 호스팅 openGym 운동 기록기처럼 단일 목적의 서비스는 자체 TLS를 제공하고 데이터베이스 파일의 위치를 확인해 두면 같은 장비에서 문제없이 함께 실행할 수 있다. 웹 애플리케이션, Postgres 데이터베이스, 백그라운드 작업 큐를 함께 사용하는 서비스는 이 범위에서 AFFiNE 쪽에 더 가깝다. 따라서 가져오기 작업 도중 한계에 도달하기 전에 자체 호스팅 Chatwoot 지원 데스크를 이 서버에 둘지 별도 서버에 둘지 결정한다. 순간적으로 부하가 증가하는 작업은 더 주의해야 한다. 가져오기 작업과 충돌하는 것은 평균 부하가 아니라 최대 부하이기 때문이다. 각 사용자에게 자체 샌드박스 에이전트를 제공하는 자체 호스팅 OneCLI를 고려한다면, 유휴 장비에서 free -h가 보여 주는 값이 아니라 qBittorrent가 최대 속도로 실행 중일 때 실제로 남는 자원과 게시된 권장 사양을 비교한다.
FAQ
Sonarr가 파일을 하드링크하지 않고 복사하는 이유는 무엇입니까?
컨테이너 관점에서 원본과 대상이 서로 다른 파일 시스템에 있기 때문입니다. /downloads 및 /tv과 같이 두 개의 별도 바인드 마운트를 사용하는 경우, 호스트 디스크 하나에서 파생되었더라도 각각 별개의 파일 시스템으로 간주됩니다. 모든 컨테이너에 단일 상위 디렉터리를 /data로 마운트하고 그 안에 다운로드와 라이브러리를 배치하면 링크가 가능해집니다. 두 파일에 대해 stat -c '%i %h %n' 명령을 실행하여 동일한 inode 번호와 2의 링크 개수를 확인하십시오.
어떤 PUID와 PGID를 사용해야 합니까?
미디어 트리를 소유한 호스트 계정의 숫자 ID를 사용하십시오. 이 값은 id -u 및 id -g 명령으로 확인할 수 있습니다. 새로 설치한 Ubuntu VPS에서는 보통 두 값 모두 1000입니다. 스택 내의 모든 컨테이너는 동일한 쌍을 사용해야 합니다. 그렇지 않으면 한 애플리케이션이 작성한 파일을 다른 애플리케이션이 수정할 수 없게 됩니다. 값을 변경한 후에는 docker compose up -d --force-recreate로 컨테이너를 재생성하고 chown -R으로 기존 파일의 권한을 수정하십시오.
웹 인터페이스를 인터넷에 직접 노출해야 합니까?
아니요, 노출해서는 안 됩니다. Compose 파일에서 각 게시된 포트를 127.0.0.1에 바인딩하십시오. 그 후 SSH 터널, VPN 또는 TLS(전송 계층 보안)를 종료하고 자체 인증을 추가하는 리버스 프록시를 통해 인터페이스에 접근하십시오. Docker는 자체 방화벽 규칙을 삽입하므로 ufw deny 규칙만으로는 해당 트래픽을 차단할 수 없습니다. 따라서 직접 노출하는 것은 매우 위험합니다.
qBittorrent 비밀번호는 어디서 찾을 수 있습니까?
LinuxServer.io 이미지는 시작 로그에 admin 사용자를 위한 임시 비밀번호를 출력합니다. docker compose logs qbittorrent | grep -i password을 실행하여 로그를 확인한 뒤, Options 및 Web UI 설정에서 영구적인 비밀번호를 지정하십시오. 사용자가 직접 비밀번호를 설정하기 전까지는 재시작할 때마다 새로운 임시 비밀번호가 생성됩니다.
Jellyfin이 동일한 폴더를 사용할 수 있습니까?
네, 그것이 이 레이아웃의 목적입니다. /mnt/data/media을 미디어 서버에 /media로 마운트하면 라이브러리는 /media/Movies 및 /media/Shows에 위치하게 되며, Sonarr와 Radarr는 /data/media를 통해 동일한 디렉터리에 파일을 작성합니다. 미디어 서버에 동일한 PUID 및 PGID을 부여하면 arr 스택이 작성한 파일을 읽을 수 있습니다.