SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-10-05

Docker Compose arr 堆疊:硬連結與共用路徑設定

在 VPS 以單一 Docker Compose 檔案執行 Prowlarr、Sonarr、Radarr 與 qBittorrent,透過共用 PUID、PGID 和 volume 路徑避免硬連結失效與檔案重複占用空間。

建置內容

Docker Compose arr 堆疊由 4 個容器組成,用來管理媒體庫:Prowlarr 負責索引器設定,Sonarr 負責影集,Radarr 負責電影,qBittorrent 則是下載用戶端。這些容器會透過 Compose network,以服務名稱互相連線,並共用主機上的同一個資料夾樹狀結構。安裝本身很簡短。這個堆疊能否長期穩定運作,或是每週都需要處理問題,關鍵在於 volume 配置。因此,本指南大多著重於此。

這個堆疊不會替你尋找內容。Prowlarr 只會保存你加入的索引器,而你使用哪些索引器,是你的決定與法律責任。本指南涵蓋的是基礎整合:使用者、路徑、權限、容器網路,以及用來確認系統正常運作的檢查。

如果你從未撰寫過 Compose 檔案,請先閱讀 VPS 的 Docker Compose 基礎知識。本文章假設 docker compose version 已經能在你的伺服器上輸出內容。

為什麼硬連結會失效,以及為什麼這是整個問題的核心

Sonarr 完成下載後,會將檔案匯入媒體庫。如果下載資料夾與媒體庫資料夾位於同一個檔案系統,匯入時會建立硬連結:新增一個名稱,指向磁碟上的同一份資料。這不會額外占用空間,也幾乎不需耗時。Torrent 會繼續從原本的名稱做種,而媒體伺服器則讀取新的名稱。

如果兩個資料夾位於不同的檔案系統,核心就無法建立這個連結。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 寫入檔案。請使用自己的帳戶,這樣就能透過 SSH 讀取及編輯這些檔案,不需要使用 sudo。

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/data

將 TZ 設定為您自己的時區,例如 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 項設定會實際影響服務運作。

${DATA_ROOT}:/data 在 3 個會處理媒體的容器中完全相同。Prowlarr 不需要這項設定,因為 Prowlarr 不會開啟媒體檔案。

所有 Web 埠都繫結至 127.0.0.1,因此 Docker 只會在 loopback 位址上發布這些埠。單獨使用 8989:8989 會讓 Docker 在所有介面上發布該埠,而且 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 ps

4 個服務都應讀取 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 連線到伺服器。這些應用程式都不應只依賴自身的登入頁面,直接暴露在公用網際網路上。若選擇反向代理,且希望四個介面共用一個帳號,而不是管理四組獨立的應用程式登入資訊,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 掛載,因此兩者對所有路徑的理解已經一致。這也是此配置值得採用的第二個原因。

將 Prowlarr 連接至 Sonarr 和 Radarr

Prowlarr 會將索引器定義推送至其他應用程式,因此只需設定一次索引器,不必重複設定。它需要各應用程式提供 API(application programming interface)金鑰。

在 Sonarr 中開啟 Settings,再開啟 General,然後複製 API 金鑰。在 Prowlarr 中開啟 Settings,再開啟 Apps,新增 Sonarr 應用程式,並填寫 3 個欄位。Prowlarr Server 是 http://prowlarr:9696。Sonarr Server 是 http://sonarr:8989。API Key 填入剛才複製的值。按下 Test。顯示綠色結果表示 Prowlarr 已透過 Compose network 連線至 Sonarr。接著在 http://radarr:7878 重複 Radarr 的設定。

若顯示連線遭拒的紅色結果,幾乎都表示服務名稱錯誤,或缺少 http:// 前綴。請確認容器內可以解析該名稱:

docker compose exec prowlarr curl -sS -o /dev/null -w '%{http_code}\n' http://sonarr:8989

HTTP status code 表示網路路徑正常。名稱解析錯誤則表示服務名稱不正確。

確認硬連結確實生效

在確認連結數之前,不要假設設定已正確。匯入一個項目後,比對下載的檔案與媒體庫中的檔案:

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 相符,並記住資料夾必須具備執行權限,容器才能進入其中。

檔案顯示由 root 擁有,表示容器在主機資料夾建立前就已啟動,因此 Docker 以 root 身分建立了該資料夾。停止 stack、chown 該資料夾,再重新啟動 stack。

從 qBittorrent 刪除 torrent 後,若發現媒體庫中的檔案也消失,表示匯入時使用的是複製,之後又刪除了複本;或是你刪除了資料,而不是 torrent 項目。真正的 hardlink 在刪除其中一個名稱後,另一個名稱仍會保留,因為只有在 link count 降至 0 時,資料才會釋放。

磁碟用量增加速度高於新增媒體的速度,是複製問題最昂貴的形式。在購買更多儲存空間前,先執行上方的 stat 檢查。

VPS 需要提供的資源

這 3 個 arr 應用程式的負載很低。它們會輪詢 indexer、寫入小型 SQLite 資料庫,並重新命名檔案。具備 2 GB RAM 的伺服器可輕鬆執行全部 4 個容器。

負載主要來自其他工作。download client 處理大型 torrent 時,會讓磁碟輸入與輸出達到飽和;同一台伺服器上的 media server 轉碼影片時,則會佔用 CPU。請將媒體檔案放在具備實際吞吐量的儲存區。如果伺服器還要執行其他重要工作,請為 download client 設定頻寬上限。

請另外為這些工作配置資源,不要假設伺服器一定有足夠餘裕:自架 AFFiNE 工作區 會再使用 4 個容器,後方還有資料庫;在 2 GB 伺服器上,它通常會佔用大部分記憶體。並非每項額外服務都需要同等資源:像 自架 openGym 運動追蹤器 這類單一用途的服務,只要配置專用 TLS,並在交付給它 1 年的訓練紀錄前確認資料庫檔案的位置,就能與其他服務共用同一台伺服器。

任何同時包含 Web 應用程式、Postgres 資料庫及背景工作佇列的服務,資源需求都更接近 AFFiNE。因此,在匯入作業執行到一半才發現資源不足前,請先決定 自架 Chatwoot 支援服務台 應部署在這台伺服器,還是應使用獨立伺服器。

突發型工作負載需要更加謹慎,因為與匯入作業衝突的是尖峰負載,而不是平均負載。如果你考慮使用 自架 OneCLI,為每位使用者提供各自的沙箱化 agent,請將其公布的規模需求與 qBittorrent 全速執行時實際可用的資源比較,不要拿閒置伺服器上 free -h 顯示的數值來比較。

FAQ

為什麼 Sonarr 會複製檔案,而不是建立硬連結?

因為從容器的角度來看,來源與目的地位於不同的檔案系統。即使兩者都來自主機上的同一顆磁碟,/downloads 與 /tv 這類兩個獨立的 bind mount 仍會被視為不同的檔案系統。在每個容器中將單一父目錄掛載為 /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 修正現有檔案的擁有權。

需要將這些 Web 介面公開到網際網路嗎?

不需要,也不應該這樣做。在 Compose 檔案中,將每個發布的連接埠綁定至 127.0.0.1,然後透過 SSH tunnel、VPN,或負責 TLS termination(傳輸層安全性)並加入自身驗證機制的反向代理存取這些介面。直接發布連接埠的風險比看起來更高,因為 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 stack 寫入的內容。

#sonarr#radarr#prowlarr#docker-compose#self-hosting