Docker Composeでarrスタックを1ファイル構築
Prowlarr、Sonarr、Radarr、qBittorrentをVPSで動かすCompose構成です。共通のPUID、PGIDとボリューム設計で、ハードリンクを機能させます。
構築するもの
Docker Compose の arr スタックは、メディアライブラリを管理する 4 つのコンテナで構成されます。インデクサー設定を管理する Prowlarr、シリーズを管理する Sonarr、映画を管理する Radarr、ダウンロードクライアントとして動作する qBittorrent です。各コンテナは Compose ネットワーク上でサービス名を使って通信し、ホスト上の 1 つのフォルダーツリーを共有します。インストール自体は短時間で完了します。スタックを何年も安定して動かせるか、毎週のように問題が発生するかを左右するのはボリューム構成です。そのため、このガイドの大部分ではボリューム構成を扱います。
このスタックがコンテンツを自動的に探してくれるわけではありません。Prowlarr には追加したインデクサーが保存されます。使用するインデクサーの選択と、その利用に関する法的責任は利用者にあります。このガイドでは、ユーザー、パス、権限、コンテナネットワーク、動作を確認するためのチェックなど、基盤部分を扱います。
Compose ファイルを一度も作成したことがない場合は、まず VPS 向け Docker Compose の基礎を読んでください。この投稿では、サーバー上で docker compose version を実行すると、すでに何らかの出力が表示されることを前提とします。
ハードリンクが機能しない理由と、そこがすべての要点である理由
Sonarr がダウンロードを完了すると、ファイルをライブラリにインポートします。ダウンロードフォルダーとライブラリフォルダーが同じファイルシステム上にある場合、インポートはハードリンクになります。これは、ディスク上の同じデータを指す 2 つ目の名前です。追加の容量も時間も必要ありません。Torrent は元の名前からシードを続け、メディアサーバーは新しい名前から読み取ります。
2 つのフォルダーが異なるファイルシステム上にある場合、カーネルはそのリンクを作成できません。Sonarr はコピーに切り替えます。40 GB のシーズンでは、ディスク使用量が 80 GB になり、入出力に数分かかります。さらに、インポートログにはハードリンクに失敗し、代わりにファイルをコピーしたことが記録されます。ディスク容量に上限がある VPS では、これが 1 週間で容量不足になる原因です。
ここに落とし穴があります。コンテナ内では、bind mount がファイルシステムの境界になります。/mnt/data/torrents を /downloads として、/mnt/data/media を /tv としてマウントすると、どちらも 1 台のホストディスク上にあっても、Sonarr からは別々のマウントとして見えます。そのため、Sonarr はそれらをまたぐリンクの作成を拒否します。LinuxServer.io の公式イメージドキュメントにも、別々の /downloads と /tv のパスを使うとハードリンク機能が失われると明記されています。
解決策は、マウントを 1 つにすることです。メディアを扱うすべてのコンテナに同じ単一ボリューム /mnt/data:/data を割り当て、各コンテナが使うパスをその中のフォルダーにします。マウントポイントが 1 つなら、ファイルシステムも 1 つになり、ハードリンクが機能します。
ユーザー、グループ、フォルダーを作成する
コンテナは、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先に進む前に、本当に 1 つのファイルシステム上にあることを確認します。
df --output=source,target /mnt/data/torrents /mnt/data/media2 行とも同じソースデバイスを示している必要があります。異なるデバイスが 2 つある場合、コンテナの設定をどう変更してもハードリンクは機能しません。
ライブラリフォルダーの名前を Movies と Shows にしているのには理由があります。すでに メディアサーバーとして Jellyfin を運用している場合、/mnt/data/media を Jellyfin に /media としてマウントすると、そのライブラリは /media/Movies と /media/Shows に配置されます。これは、そのガイドで指定されている場所と同じです。
環境ファイル
サーバーごとに変わる値は、Compose ファイルと同じ場所にある .env に保存します。
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このファイルでは、4 つの設定が実際の動作を担っています。
メディアを扱う 3 つのコンテナでは、${DATA_ROOT}:/dataが同一です。Prowlarr はメディアファイルを開かないため、${DATA_ROOT}:/dataを設定しません。
すべての Web ポートは127.0.0.1にバインドされるため、Docker はループバックアドレスでのみ公開します。単純な8989:8989ではすべてのインターフェイスで公開され、Docker 自身のファイアウォールルールによって、そのトラフィックが ufw のdenyルールをそのまま通過します。この動作は頻繁に問題になります。詳しくはDocker が ufw を通過してポートを直接公開する理由を参照してください。
ポート 6881 は意図的にすべてのインターフェイスで公開します。これは torrent の待ち受けポートであり、着信するピア接続から到達できる必要があります。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 ps4 つのサービスはすべてrunningを読み込む必要があります。2026 年 7 月時点で、これらのイメージはlscr.ioで公開されており、latestタグは現在の安定版リリースに追従します。アップグレードを予期せぬ変更ではなく判断に基づくものにしたい場合は、代わりにバージョンタグを固定してください。
Web インターフェースへ安全にアクセスする
ポートは loopback で待ち受けているため、まだ外部には公開されていません。自分のマシンから 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 にアクセスできます。恒久的にアクセスする場合は、複数のアプリ用 TLS 証明書に対応した Traefik の背後にスタックを配置するか、自分でホストする WireGuard VPN 経由でサーバーに接続します。これらのアプリケーションを、各アプリケーション自身のログイン画面だけで保護してパブリックインターネットに公開してはいけません。リバースプロキシを使用し、4 つのインターフェースごとに別々のアプリケーションログインを管理する代わりに 1 つのアカウントで統一したい場合は、Authentik によりセルフホスト型のシングルサインオンを構成できます。Traefik は forward auth により、すべてのリクエストに対してこれを適用できます。
qBittorrent は初回起動時にランダムな管理者パスワードを生成し、コンテナのログに出力します。ログでパスワードを確認し、Web インターフェースで変更します。
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 は内部 DNS(domain name system)サービスを使用して、4 つのコンテナを同じネットワークに接続するため、サービス名をホスト名として使用できます。ここで localhost は使用しないでください。Sonarr コンテナ内では、localhost は Sonarr を指します。
Remote Path Mappings は空のままにします。この機能は、ダウンロードクライアントが報告するパスを、arr アプリケーションが参照できるパスに変換するためのものです。共有 /data マウントを1つ使用すれば、両方のコンテナがすでにすべてのパスを共有できます。これが、この構成に手間をかける2つ目の理由です。
Prowlarr を Sonarr と Radarr に接続する
Prowlarr は indexer の定義を他のアプリケーションへ送信するため、indexer を 2 回設定する必要がありません。各アプリケーションから API(application programming interface)key を取得する必要があります。
Sonarr で Settings、General の順に開き、API key をコピーします。Prowlarr で Settings、Apps の順に開き、Sonarr application を追加して、3 つのフィールドに入力します。Prowlarr Server は http://prowlarr:9696 です。Sonarr Server は http://sonarr:8989 です。API Key にはコピーした値を入力します。Test を押します。緑色の結果が表示されれば、Prowlarr から Compose network 経由で Sonarr に接続できています。Radarr でも http://radarr:7878 を使用して同じ手順を繰り返します。
接続が拒否されたことを示す赤色の結果は、ほとんどの場合、サービス名が誤っているか、http:// prefix が欠落していることを意味します。container 内から名前を解決できることを確認します。
docker compose exec prowlarr curl -sS -o /dev/null -w '%{http_code}\n' http://sonarr:8989HTTP status code が返れば、network path は正常です。名前解決エラーが返る場合は、サービス名が誤っています。
ハードリンクが実際に作成されたことを確認する
リンク数を確認するまで、設定が正しいとは判断しないでください。1 つの項目をインポートしたら、ダウンロードしたファイルとライブラリ内のファイルを比較します。
stat -c '%i %h %n' /mnt/data/torrents/tv/*/*.mkv
stat -c '%i %h %n' /mnt/data/media/Shows/*/*/*.mkv最初の番号は inode、2 番目の番号はリンク数です。ハードリンクされたファイルでは、両方の場所で同じ inode が表示され、リンク数は 2 になります。異なる inode がそれぞれリンク数 1 で表示される場合、Sonarr はファイルをコピーしています。インポートログには、ハードリンクに失敗したことが記録されます。
ディスク使用量も確認してください。インポート時に df -h /mnt/data はほとんど変化しないはずです。ハードリンクでは名前が追加されるだけで、データは追加されないためです。
実際に壊れるもの
インポート時の権限エラーは、コンテナのユーザー ID がライブラリフォルダーに書き込めないことを示します。メッセージは Access to the path ... is denied です。ls -ln /mnt/data/media で所有者 ID が PUID と一致することを確認してください。また、コンテナがディレクトリに入るには、ディレクトリに実行権限が必要です。
root が所有者として表示されるファイルは、ホストのディレクトリが存在する前にコンテナが起動したことを示します。そのため、Docker がそのディレクトリを root として作成しました。スタックを停止し、ディレクトリを chown してから、再度起動してください。
qBittorrent で torrent を削除した後にライブラリのファイルもなくなっている場合、インポートが後で削除されたコピーだったか、torrent エントリではなくデータを削除した可能性があります。実際のハードリンクであれば、1 つの名前を削除しても、もう一方は残ります。データが解放されるのは、リンク数が 0 になったときだけです。
追加したメディアの容量より速くディスクが一杯になる場合、コピーによる問題が最も大きな形で現れています。ストレージを追加購入する前に、上記の stat チェックを実行してください。
VPS に必要なリソース
3 つの arr アプリケーションは軽量です。インデクサーをポーリングし、小規模な SQLite データベースに書き込み、ファイル名を変更します。RAM 2 GB のサーバーであれば、4 つすべてのコンテナを無理なく実行できます。負荷は別の処理から発生します。大容量 torrent のダウンロード中は、ダウンロードクライアントがディスクの読み書きを飽和させます。同じホストでメディアサーバーが動画をトランスコードすると、CPU も消費します。メディアは実効スループットの高いボリュームに保存し、サーバーで他の重要な処理も行う場合は、ダウンロードクライアントに帯域幅制限を設定してください。余裕があると決めつけず、他の処理に必要なリソースは別に見積もってください。セルフホストの AFFiNE ワークスペースは、データベースを含む別の 4 つのコンテナを必要とし、RAM 2 GB のホストではメモリの大半を使用します。追加サービスのすべてが同じ量のリソースを必要とするわけではありません。セルフホストの openGym ワークアウトトラッカーのような単一目的のサービスであれば、独自の TLS を設定し、1 年分のトレーニング履歴を保存する前にデータベースファイルの場所を把握しておけば、同じホストで問題なく運用できます。Web アプリケーション、Postgres データベース、バックグラウンドワーカーキューを含むサービスは、この範囲では AFFiNE 側に近いリソースを必要とします。そのため、インポートの途中で上限に達する前に、セルフホストの Chatwoot サポートデスクをこのサーバーで運用するか、専用サーバーに分けるかを決めてください。バースト性のあるワークロードには、さらに注意が必要です。インポートと衝突するのは平均負荷ではなくピーク負荷だからです。各ユーザーにサンドボックス化したエージェントを割り当てるセルフホストの OneCLIを検討している場合は、qBittorrent が最大速度で動作しているときに実際に空いているリソースと、アイドル状態のホストで free -h が表示する値を比較して、公開されているサイジング情報を確認してください。
FAQ
Sonarr がファイルをハードリンクせず、コピーするのはなぜですか?
コンテナから見ると、ソースと宛先が異なるファイルシステム上にあるためです。/downloads と /tv のような別々の bind mount は、同じホストディスクを使用していても別のファイルシステムとして扱われます。すべてのコンテナで単一の親ディレクトリを /data としてマウントし、その中にダウンロードとライブラリを配置すると、リンクを作成できるようになります。両方のファイルに対して stat -c '%i %h %n' を実行し、同じ inode と 2 のリンク数になっていることを確認します。
使用する PUID と PGID は何ですか?
メディアツリーを所有するホストアカウントの数値 ID を使用します。ID は id -u と id -g で確認できます。新規の Ubuntu VPS では、通常はどちらも 1000 です。スタック内のすべてのコンテナで同じ組み合わせを使用する必要があります。そうしないと、あるアプリケーションが、別のアプリケーションで変更できないファイルを書き込むことになります。値を変更したら、docker compose up -d --force-recreate でコンテナを再作成し、chown -R で既存ファイルの所有者とグループを修正します。
これらの Web インターフェースをインターネットに公開する必要はありますか?
いいえ。公開すべきではありません。Compose ファイルで各公開ポートを 127.0.0.1 にバインドし、SSH トンネル、VPN、または TLS(transport layer security)を終端して独自の認証を追加するリバースプロキシ経由でインターフェースにアクセスします。直接公開すると、見た目以上に危険です。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 スタックが書き込んだファイルを読み取れるようにします。