SSD Nodes Learn 8GB 内存 — 每年 $66
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-01

Docker Compose 一文件部署 Arr 堆栈

用一个 Docker Compose 文件在 VPS 上运行 Prowlarr、Sonarr、Radarr 和 qBittorrent,统一 PUID、PGID 与卷路径,避免硬链接失效后复制文件。

你要构建的内容

Docker Compose arr 堆栈由 4 个容器组成,用于管理媒体库:Prowlarr 管理索引器设置,Sonarr 管理剧集,Radarr 管理电影,qBittorrent 作为下载客户端。它们通过 Compose 网络使用服务名称相互通信,并共享主机上的一个目录树。安装过程很短。决定该堆栈能否长期稳定运行,还是每周都需要处理问题的关键在于卷布局,因此本指南的大部分内容都围绕这一点展开。

该堆栈不会替你查找内容。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,并且它们使用的每个路径都必须是该卷中的目录。一个挂载点,一个文件系统,硬链接即可正常工作。

创建用户、组和目录

容器会使用由 PUIDPGID 设置的数字用户 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

两行必须显示相同的源设备。不同的设备意味着硬链接永远无法工作,无论您如何设置容器配置。

库目录特意命名为 MoviesShows。如果您已经运行 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 仅在回环地址上发布这些端口。直接使用 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 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 连接服务器。任何一个应用都不应仅依靠自身的登录页面直接暴露在公共互联网中。

qBittorrent 首次启动时会生成随机管理员密码,并将其输出到容器日志中。读取该密码,然后在 Web 界面中修改:

docker compose logs qbittorrent | grep -i password

如果跳过修改,每次重启都会生成新的随机密码,您每次都必须回到日志中查看密码。

设置每个应用中的路径

在 qBittorrent 中打开“选项”,然后进入“下载”,将默认保存路径设置为 /data/torrents。将未完成下载文件夹放在同一目录树中,例如 /data/torrents/incomplete。下载任务如果在 /data 之外完成,就无法通过硬链接添加到媒体库。

在 Sonarr 中打开“设置”,然后进入“媒体管理”,添加根文件夹 /data/media/Shows。Radarr 的根文件夹为 /data/media/Movies。这些都是容器内的路径。主机路径 /mnt/data/media/Shows 会被拒绝,因为从容器的角度看,该目录不存在。

在 Sonarr 和 Radarr 中,都打开“设置”,然后进入“下载客户端”,添加 qBittorrent。主机为 qbittorrent,端口为 8080。服务名称可以用作主机名,因为 Compose 会将这 4 个容器放到同一个网络中,并提供内部 DNS(域名系统)服务。此处不要使用 localhost:在 Sonarr 容器内,localhost 是 Sonarr。

将“远程路径映射”留空。此功能用于将下载客户端报告的路径转换为 arr 应用可以访问的路径。由于两个容器都挂载了同一个 /data,它们已经对所有路径使用相同的映射方式。这也是这种目录布局值得采用的第二个原因。

将 Prowlarr 连接到 Sonarr 和 Radarr

Prowlarr 会将索引器定义推送到其他应用,因此只需配置一次索引器,而不必配置两次。它需要从每个应用获取一个 API(应用程序编程接口)密钥。

在 Sonarr 中,打开 Settings,然后打开 General,复制 API key。在 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:8989

HTTP 状态码证明网络路径正常。名称解析错误证明服务名称不正确。

验证硬链接确实已创建

在看到链接计数之前,不要认为设置已生效。导入一个项目后,比较下载文件和媒体库文件:

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 身份创建了该目录。停止堆栈,chown 该目录,然后重新启动堆栈。

从 qBittorrent 删除 torrent 后发现媒体库文件也消失,表示导入使用的是副本,之后该副本被删除;或者您删除了数据,而不是 torrent 条目。使用真正的硬链接时,删除其中一个名称不会影响另一个名称,因为只有当链接计数降至零时,数据才会被释放。

磁盘空间的增长速度超过您添加媒体的速度,说明副本问题已经以最严重的形式出现。在购买更多存储前,先运行上面的 stat 检查。

此技术栈对 VPS 的要求

这 3 个 arr 应用的资源占用很低。它们会轮询索引器、写入小型 SQLite 数据库并重命名文件。配备 2 GB RAM 的服务器可以稳定运行全部 4 个容器。负载主要来自其他服务。下载客户端处理大型 torrent 时会占满磁盘 I/O;如果同一台服务器上的媒体服务器正在转码视频,也会占用 CPU。请将媒体存储在具有实际吞吐能力的卷上。如果服务器还要执行其他重要任务,请为下载客户端设置带宽限制。

FAQ

为什么 Sonarr 会复制文件,而不是创建硬链接?

因为从容器的角度看,源目录和目标目录位于不同的文件系统中。两个独立的 bind mount(例如 /downloads/tv)会被视为两个文件系统,即使它们都来自同一块主机磁盘。将单个父目录作为 /data 挂载到每个容器中,并将下载目录和媒体库放在该目录内,这样才能创建链接。对两个文件运行 stat -c '%i %h %n',确认它们使用相同的 inode,且链接计数为 2

应使用哪些 PUID 和 PGID?

使用媒体目录所有者对应的主机账户的数字 id,可通过 id -uid -g 获取。在全新的 Ubuntu VPS 上,这两个值通常都是 1000。堆栈中的每个容器都必须使用相同的一对值,否则一个应用写入的文件可能无法被另一个应用修改。更改这些值后,使用 docker compose up -d --force-recreate 重新创建容器,并使用 chown -R 修复现有文件。

是否需要将这些 Web 界面暴露到互联网?

不需要,也不应这样做。在 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 向这些相同目录写入文件。为媒体服务器设置相同的 PUIDPGID,使其能够读取 arr 堆栈写入的内容。

#sonarr#radarr#prowlarr#docker-compose#自托管