Docker Compose 部署 Prowlarr、Sonarr、Radarr 和
用一个 Docker Compose 文件在 VPS 上运行 Prowlarr、Sonarr、Radarr 和 qBittorrent,统一 PUID、PGID 与卷布局,避免硬链接失效后复制文件。
构建内容
Docker Compose arr stack 由 4 个容器组成,用于管理媒体库:Prowlarr 管理索引器设置,Sonarr 管理剧集,Radarr 管理电影,qBittorrent 作为下载客户端。它们通过 Compose 网络中的服务名相互通信,并共享主机上的同一目录树。安装过程很短。决定该 stack 能否长期稳定运行、还是每周都需要处理问题的关键,是卷的布局,因此本指南的大部分内容都围绕这一点展开。
该 stack 不会替您查找内容。Prowlarr 中保存的是您添加的索引器,使用哪些索引器由您自行决定,相关法律责任也由您承担。本指南介绍基础配置:用户、路径、权限、容器网络,以及用于确认系统正常运行的检查方法。
如果您从未编写过 Compose 文件,请先阅读 VPS 的 Docker Compose 基础知识。本文章假定 docker compose version 已经能在您的服务器上输出内容。
为什么硬链接会失效,以及为什么这至关重要
当 Sonarr 完成下载后,它会将文件导入到您的媒体库中。如果下载文件夹和媒体库文件夹位于同一个文件系统上,导入操作会使用硬链接:即指向磁盘上同一数据的第二个名称。这不会占用额外的空间,也不会产生时间开销。种子会继续通过旧名称进行做种,而您的媒体服务器则读取新名称的文件。
如果这两个文件夹位于不同的文件系统上,内核将无法创建该链接。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/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 的监听端口,必须可访问,才能接收对端连接。这里同样适用 Docker 的上述行为,因此不需要为它添加 ufw 规则,添加规则也不会改变任何结果。Docker 文档在 2026 年 10 月核对时说明,发往已发布端口的流量会在到达 ufw 使用的链之前完成路由,因此 sudo ufw allow 6881 对此端口没有作用。容器启动后,该端口就可从互联网访问;ufw 的 deny 也无法关闭它。如果需要限制允许连接的来源,Docker 文档建议在其 DOCKER-USER 链中配置规则,而不是使用 ufw。对于不运行在 Docker 中的服务(例如 SSH),继续使用 ufw;如果不熟悉该工具,请阅读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 年 10 月,这些镜像发布在 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 访问服务器。不过,VPN 本身无法访问这些端口。Docker 文档于 2026 年 10 月核查后说明:当端口发布到 localhost 地址时,只有 Docker 主机可以访问该端口。若要通过 VPN 使用这些服务,可以继续通过 VPN 运行 SSH 隧道,或将 Compose 文件中的绑定地址从 127.0.0.1 改为服务器 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(域名系统)服务将这4个容器连接到同一网络,因此可以使用服务名作为主机名。此处不要使用 localhost:在 Sonarr 容器中,localhost 指向 Sonarr。
将 Remote Path Mappings 留空。此功能用于将下载客户端报告的路径转换为 arr 应用可以访问的路径。两个容器共用同一个 /data 挂载后,它们已经对所有路径保持一致;这也是该布局值得采用的第二个原因。
将 Prowlarr 连接到 Sonarr 和 Radarr
Prowlarr 会将索引器定义推送到其他应用,因此只需配置一次索引器,不必重复配置。每个应用都需要提供一个 API(应用程序接口)密钥。
在 Sonarr 中打开 Settings,然后打开 General,复制 API key。在 Prowlarr 中打开 Settings,然后打开 Apps,添加 Sonarr 应用,并填写3个字段。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 状态码证明网络路径正常。名称解析错误证明服务名称不正确。
验证硬链接是否实际生效
在看到链接计数之前,不要认为配置已经生效。导入一个项目后,比较下载文件和媒体库文件:
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 删除种子后,如果发现媒体库文件也消失,说明导入时执行的是复制,之后又删除了副本;或者你删除的是数据,而不是种子条目。使用真正的硬链接时,删除其中一个名称不会影响另一个名称,因为只有链接计数降为 0 时,数据才会被释放。
如果磁盘的增长速度超过你添加媒体的速度,说明复制问题已经造成了最严重的存储浪费。购买更多存储前,先运行上文的 stat 检查。
VPS 需要满足的条件
这 3 个 arr 应用都很轻量。它们会轮询索引器、写入小型 SQLite 数据库并重命名文件。一台配备 2 GB RAM 的服务器可以轻松运行全部 4 个容器。负载主要来自其他组件。下载客户端处理大型 torrent 时会占满磁盘输入输出;同一台服务器上的媒体服务器转码视频时则会占用 CPU。如果服务器还要运行其他重要服务,请将媒体文件放在具有实际吞吐能力的卷上,并为下载客户端设置带宽限制。应单独为这些其他服务预留资源,不要假设服务器一定有足够余量:自托管 AFFiNE 工作区还包括另外 4 个容器及其后端数据库,在 2 GB 服务器上,它会占用大部分内存。并非每个附加服务的资源开销都这么高:像自托管 openGym 训练记录器这样的单一用途服务可以与其他服务共享服务器,只要为它配置独立的 TLS,并在让它保存一整年的训练记录前确认数据库文件的位置。任何同时包含 Web 应用、Postgres 数据库和后台工作队列的服务,其资源需求都更接近 AFFiNE;因此,在导入任务运行到一半才发现资源上限之前,应先决定自托管 Chatwoot 客服台应该部署在这台服务器上,还是使用独立服务器。对于突发型工作负载,还需要更加谨慎,因为与导入任务发生资源冲突的是峰值负载,而不是平均负载:如果你正在考虑自托管 OneCLI,为每个人提供独立的沙箱化代理,请将其公布的配置需求与 qBittorrent 全速运行期间实际可用的资源进行比较,而不要与空闲服务器上 free -h 显示的数值比较。
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 修复现有文件的所有权。
是否需要将这些 Web 界面暴露到互联网?
不需要,也不应这样做。在 Compose 文件中,将每个发布的端口绑定到 127.0.0.1,然后通过 SSH 隧道访问这些界面,或通过服务器上的反向代理访问。反向代理负责终止 TLS(传输层安全)并添加自身的身份验证。仅使用 VPN 无法访问绑定到 127.0.0.1 的端口,因为 Docker 只允许宿主机本身访问这些端口。如果需要通过 VPN 直接访问,应将端口发布到服务器的 VPN 地址,而不是直接发布到公网接口。直接发布到公网接口的风险比看起来更高,因为 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 堆栈写入的文件。