文章

树莓派 homelab 全景:Jellyfin 家庭影院、Vaultwarden 密码与统一入口的演进

在树莓派 homelab 上补装 Jellyfin、Jellyseerr、Vaultwarden、Homepage 四个服务,把自动下载管线闭环成完整的家庭影院;并把入口方案从裸端口、Caddy 子路径一路演进到 Homepage 占根路径的最终形态。

树莓派 homelab 全景:Jellyfin 家庭影院、Vaultwarden 密码与统一入口的演进
  1. 1. 背景:下载管线缺了“两头”,入口也乱了
  2. 2. Jellyfin:补上“看”的一头
    1. 2.1 已经有 Kodi + Samba 了,为什么还要 Jellyfin?
    2. 2.2 部署
  3. 3. Jellyseerr:补上“点播”的一头
  4. 4. Vaultwarden:密码到底该怎么管
    1. 4.1 先拆掉一个错误前提
    2. 4.2 部署:十分钟跑起来,然后撞上 HTTPS
    3. 4.3 顺着 HTTPS 追问:零知识加密模型
    4. 4.4 服务器不解密,那它到底有什么用?
    5. 4.5 最后一个环节:密码是怎么“到” Radarr 的?
  5. 5. 顺带弄清:LinuxServer.io 是什么、和官方镜像差在哪
  6. 6. 统一入口的三次演进:从裸端口到 Homepage 占根
    1. 6.1 阶段一:裸端口,记不住
    2. 6.2 阶段二:Caddy 子路径收敛
    3. 6.3 阶段三:想通了——入口只需要一个
    4. 6.4 三轮方案对比
  7. 7. 当前栈全景与下一步

1. 背景:下载管线缺了“两头”,入口也乱了

之前的文章里,树莓派上已经有了一条自动化下载管线:Radarr 管电影、Jackett 聚合种子站、qBittorrent 下载、Bazarr 和 ChineseSubFinder 补字幕,后来又修复了硬链接失效导致的磁盘翻倍问题

但用了一段时间会发现,这条管线只解决了“下”的问题,两头都缺,还附带两个管理问题:

  • 上游缺“发现”:想看电影要自己打开 Radarr 搜索、选画质,家里人根本不会用;
  • 下游缺“播放”:片子入库后靠 Samba 共享裸文件播放,没有海报墙、没有进度记录、没有手机客户端;
  • 入口混乱:服务越装越多,七八个 WebUI 端口记不住,每次都要翻备忘录;
  • 密码裸奔:各服务的密码和 API Key 明文写在文档甚至公开博客里。

这次补装的四个服务正好一一对应:Jellyseerr 管“发现”,Jellyfin 管“播放”,Vaultwarden 管“密码”,Homepage 管“入口”。

flowchart LR
    Family[家人] -->|搜片点想看| JS[Jellyseerr 5055]
    JS -->|下发任务| Radarr[Radarr 7878]
    Radarr --> Jackett[Jackett 9117]
    Radarr --> qBT[qBittorrent 8085]
    qBT --> Downloads[(/share/Downloads)]
    Radarr -->|硬链接入库| Movies[(/share/Movies)]
    Movies --> JF[Jellyfin 8096]
    JF -->|海报墙/播放| Family
    HP[Homepage 3001] -.->|统一入口| Family
    VW[Vaultwarden 8443] -.->|密码与 API Key| Family
    style JS fill:#e3f2fd
    style JF fill:#e8f5e9
    style HP fill:#fff3bf
    style VW fill:#fff3bf

四个服务均已提交到 pi-docker-homelab 仓库的 compose 里。下面就按这几个缺口的顺序逐个补上——先是“看”和“点”,再是“密码”,最后单用一节讲“入口”的三次演进,那是这次折腾真正想明白的东西。

2. Jellyfin:补上“看”的一头

2.1 已经有 Kodi + Samba 了,为什么还要 Jellyfin?

先把现状说清楚:在此之前,我的播放链路是 Radarr 下载入库 → 电视上的 Kodi 通过 Samba 直连树莓派 → 选文件播放。这条链路是自洽的——Kodi 自己也能刮削海报墙、记录本机进度。所以如果场景永远只有“一台电视、一个观众”,Jellyfin 的价值确实有限,这话得先说在明处。

Jellyfin 是开源媒体服务器,它和 Kodi 的本质区别在于媒体库放在哪

  • Kodi + Samba 是“本地媒体库”:海报墙、刮削数据、观看进度都存在那台电视本地,是它一台的私有财产;
  • Jellyfin 是“服务端媒体库”:扫描片库、刮削元数据、记录进度全部发生在服务端,所有客户端共享同一份。

这个区别带来几个实际收益:

  • 跨设备续播:电视上看到一半,躺床上用手机、用电脑网页能从同一秒接着看。Kodi 的进度只认识那台电视;
  • 多设备零配置:手机、平板、电脑想看片,浏览器打开就是全部,不用每台设备装 Kodi、配 Samba;
  • 多用户互不干扰:家人各自有账号、各自的进度和列表,儿童账号还能限制内容范围;
  • 和 Jellyseerr 闭环:下一节点播链的终点就是 Jellyfin——家人点完“想看”,片子下好后自动出现在海报墙上,不用再去电视上翻文件夹。

关键是这两者不冲突,可以合体:Kodi 有官方插件 Jellyfin for Kodi,电视继续用 Kodi 的播放器(它的解码能力比 Jellyfin 自家客户端强),但媒体库、海报墙、观看进度全部交给 Jellyfin 服务端统一管理。所以正确的定位是把 Jellyfin 当后台媒体库,而不是电视播放器的替代品——电视上的习惯可以一点都不改,白赚多端同步。

2.2 部署

linuxserver/jellyfin 镜像与现有栈保持同一套约定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  jellyfin:
    image: linuxserver/jellyfin:latest
    container_name: jellyfin
    environment:
      - PUID=1000
      - PGID=100
      - TZ=Asia/Shanghai
      # 刮削元数据需要访问 TMDB,走宿主机 V2Ray 代理
      - HTTP_PROXY=http://172.18.0.1:10809
      - HTTPS_PROXY=http://172.18.0.1:10809
      - NO_PROXY=localhost,127.0.0.1,jellyseerr,radarr
    volumes:
      - /home/pi/docker/jellyfin/config:/config
      - /home/pi/docker/jellyfin/cache:/cache
      # 只读挂载片库即可,元数据写在 config 里
      - /share/Movies:/data/movies:ro
    ports:
      - "8096:8096"
    restart: unless-stopped

两个要点:

  • 片库只读挂载:ro),Jellyfin 的元数据、字幕缓存都写在自己的 config/cache 里,不会污染片库目录;
  • 刮削要访问 TMDB,和 Jackett 一样走宿主机 V2Ray 代理,否则海报和简介拉不下来。

首次访问 http://raspberrypi.local:8096 创建管理员,添加媒体库时选容器内路径 /data/movies。性能上树莓派 1080p 直接播放毫无压力,但不要指望它做 4K 实时转码——客户端支持直接播放才是关键。

3. Jellyseerr:补上“点播”的一头

片子能看了,下一个问题是“看什么”从哪来——总不能要求每个家人都学会 Radarr。Jellyseerr 是面向 Jellyfin 生态的点播门户(Plex 生态对应的叫 Overseerr)。家人打开网页搜电影、点“想看”,请求自动流转到 Radarr 下载、入库、出现在 Jellyfin 里,全程不用知道 Radarr 的存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  jellyseerr:
    image: fallenbagel/jellyseerr:latest
    container_name: jellyseerr
    environment:
      - TZ=Asia/Shanghai
      # 搜索/海报依赖 TMDB API,走宿主机 V2Ray 代理
      - HTTP_PROXY=http://172.18.0.1:10809
      - HTTPS_PROXY=http://172.18.0.1:10809
      - NO_PROXY=localhost,127.0.0.1,jellyfin,radarr
    volumes:
      - /home/pi/docker/jellyseerr/config:/app/config
    ports:
      - "5055:5055"
    restart: unless-stopped

首次访问 http://raspberrypi.local:5055 时选 Jellyfin 登录(用 Jellyfin 的管理员账号),然后在设置里关联 Radarr(Host 填 radarr,端口 7878,API Key 从 Radarr 设置页复制)。同样要注意 TMDB 走代理,否则搜索页一片空白。

4. Vaultwarden:密码到底该怎么管

前两个服务解决的是“用”的问题,这一节解决“管”的问题——而且它牵出的东西比预想的多:从部署时的一个报错出发,一路追问到了整套加密模型,最后才弄明白密码到底是怎么被填进各个服务的。

4.1 先拆掉一个错误前提

之前的做法是密码明文写在文档里,理由是“反正都是内网,别人看不到;就算看到也登录不了”。这个理由有个事实漏洞:密码不只是写在本地文档里,而是明文出现在公开发布的博客文章里,全世界都能搜到。

真正在起保护作用的并不是“别人看不到”,而是服务只监听内网——互联网上的陌生人拿到密码也够不着 192.168.1.7。但仍然剩下两类真实风险:

  • 局域网内部:访客 WiFi 的设备、中毒的手机,拿着公开密码可以登录全部服务;
  • 密码复用习惯pi123456 这类弱密码如果和其他账号有复用,公开它等于公开别处的钥匙。

4.2 部署:十分钟跑起来,然后撞上 HTTPS

Vaultwarden 是 Bitwarden 的轻量自托管实现(Rust 编写,树莓派上跑得很轻松)。它承诺的改变很诱人:浏览器/手机自动填充、每个服务一个随机强密码、API Key 和 TOTP 也能一并收进来。compose 照例很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    environment:
      - TZ=Asia/Shanghai
      # 初次使用先开放注册,建好账号后改为 false 重建容器
      - SIGNUPS_ALLOWED=true
      - HTTP_PROXY=http://172.18.0.1:10809
      - HTTPS_PROXY=http://172.18.0.1:10809
      - NO_PROXY=localhost,127.0.0.1
    volumes:
      # 保险库数据,务必纳入备份
      - /home/pi/docker/vaultwarden/data:/data
    ports:
      - "8001:80"
    restart: unless-stopped

但部署完第一次打开就撞上一个硬限制:Vaultwarden 必须走 HTTPS 访问。HTTP 直连 :8001 会直接报 “You are not using a secure context … You need to enable HTTPS!”。原因是它的 Web 保险库依赖浏览器的 Web Crypto API(SubtleCrypto)在本地做加解密,而浏览器只在安全上下文(HTTPS 或 localhost)里开放这个 API。Vaultwarden 不支持子路径,所以解法是让 Caddy 给它单独开一个 HTTPS 端口反代(Caddy 会自动生成本地证书):

raspberrypi.local:8443 {
    reverse_proxy vaultwarden:80
}

浏览器首次访问接受自签证书即可。完整流程:访问 https://raspberrypi.local:8443 注册主账号 → 浏览器装 Bitwarden 扩展、自托管服务器地址填同一个 HTTPS 地址 → 把各服务密码逐个换强并入库 → 把 SIGNUPS_ALLOWED 改为 false 重建容器,关掉公开注册。

先停一下——为什么一个密码管理器非要“在本地做加解密”不可? 这个问题值得追到底,因为它的答案就是 Bitwarden 的整个安全模型。

4.3 顺着 HTTPS 追问:零知识加密模型

先纠正一个理解偏差:Vaultwarden 只是服务端。它是 Bitwarden 服务端协议的 Rust 重实现(官方服务端是 C# 写的,很重量级),而浏览器扩展、手机 App、桌面客户端全是 Bitwarden 官方客户端——Vaultwarden 把协议实现得足够兼容,官方客户端连上来感觉不到区别。所以要理解它,理解 Bitwarden 的加密模型即可。

这个模型的出发点是零知识:服务器(哪怕是你自己架的这台)永远接触不到明文,也接触不到能解密的主密钥,所有加解密都在客户端本地完成。你打开的 Web 保险库页面本身就是一个加密程序——这正是它强制 Web Crypto API、进而强制 HTTPS 的原因。注册和解锁时,客户端在本地跑一条密钥派生链:

1
2
3
4
5
6
7
8
主密码 + 邮箱(作盐)
   │  PBKDF2-SHA256,默认 60 万轮迭代(可选 Argon2id)
   ▼
主密钥(Master Key)            ← 只存在于设备内存,从不传输
   │
   ├─ HKDF 拉伸 ──► 用于加密“保险库密钥”
   │
   └─ 再跑 1 轮 PBKDF2 ──► 主密码哈希 ← 登录时发给服务器的只有它

保险库密钥是注册时客户端随机生成的一把 AES-256 密钥,每条密码、每个 API Key 都靠它加密;它自己被主密钥加密后才上传服务器。主密码哈希只用于登录校验,从它反推不出主密钥。整个生命周期里服务器经手的东西:

sequenceDiagram
    participant C as 客户端
    participant S as Vaultwarden 服务器

    Note over C,S: 注册(仅一次)
    C->>C: 主密码 + 邮箱(盐)经 PBKDF2 派生主密钥
    C->>C: 随机生成保险库密钥,用主密钥加密
    C->>S: 上传邮箱、主密码哈希、加密后的保险库密钥
    Note over S: 只存密文<br>永远见不到主密码和主密钥

    Note over C,S: 日常解锁
    C->>S: 邮箱 + 主密码哈希,请求登录
    S-->>C: 校验通过,返回加密保险库
    C->>C: 本地重新派生主密钥,解出保险库密钥
    C->>C: 逐条解密密码条目,供自动填充

所谓“同步”只是密文的上传下载,解密永远发生在你自己的设备上。

4.4 服务器不解密,那它到底有什么用?

理解上面的模型后,很容易冒出一个疑问:服务器既然不参与解密,那它存在的意义是什么?

答案是密文的存储 + 多端同步。当你在手机上想登录 Radarr 时,服务器把加密后的 Radarr 条目返回给手机,手机在本地解出明文——服务器解决的是“手机、电脑、平板随时拿到同一份保险库”,而不是“帮你解密”。注意它不是备份:密文保险库只存在 vaultwarden/data 这一处(默认 SQLite),它刻意不进 git,也没有“云端另一份”,丢了就是全丢,所以这份目录必须另有备份副本。

从这个模型还能直接推出几条“规矩”:

  • 忘了主密码 = 数据永久锁死:服务器端没有任何恢复手段,这是“零知识”的字面含义。主密码要足够强、且真的记得住,别存在任何文档里;
  • 主密码必须强:就算树莓派被人整个拖库,攻击者拿到的也只有密文,只能离线暴力猜主密码。60 万轮 PBKDF2 是专门拖慢猜测速度的,但 pi123456 这种密码多少轮迭代都救不了;
  • 每个服务可以放心用随机强密码:记密码的事被客户端本地解密 + 自动填充彻底接管,人只需要记住主密码一个。纯内网时这是好习惯,一旦哪天把服务通过 VPN 或公网暴露出去,这就是生死线。

4.5 最后一个环节:密码是怎么“到” Radarr 的?

还剩最后一跳没说清楚:客户端在本地解出了明文,它怎么把这个密码交给 Radarr? 答案可能出乎意料——密码管理器从来不和目标服务通信。它做的事到“把明文填进登录表单”为止,之后的一切和你亲手打字完全相同:

sequenceDiagram
    participant BW as Bitwarden 扩展
    participant B as 浏览器
    participant R as Radarr 服务器

    BW->>BW: 本地解密出明文
    BW->>B: 填入网页登录表单
    B->>R: 正常提交表单(与手打相同)
    Note over BW: 到这一步就结束了

浏览器扩展做的就是这件事:检测当前页面的登录表单,按网址匹配条目,本地解密后填进去。没有扩展也完全能用,只是换成手动版:打开 Web 保险库或桌面/手机 App(它们和扩展是同一账号、连同一服务器,各自独立可用)→ 搜索 radarr → 点“复制密码” → 切回登录页粘贴。两个实用细节:复制优先于查看——密码全程不进肉眼,Bitwarden 还会定时自动清空剪贴板,只有在电视这类没法粘贴的设备上才点眼睛图标看明文;手机上没有扩展,对应的是系统级自动填充(Android Autofill、iOS 密码自动填充),效果相同。

也正因为最后一步就是普通表单提交,密码在局域网里仍以 HTTP 明文发给各服务——这和你以前手打密码的风险完全一样,密码管理器既没有加重、也没有解决这一跳。它管的是“存储和填写”,传输环节的安全取决于服务本身用不用 HTTPS。

最后强调一句:换工具之外,博客别再贴真实密码——工具换不掉习惯。

5. 顺带弄清:LinuxServer.io 是什么、和官方镜像差在哪

这次加的镜像里,linuxserver/jellyfin 又一次出现。这个前缀值得专门搞清楚:LinuxServer.io 是一个社区维护的 Docker 镜像团队,把流行的自建应用统一打包、以 linuxserver/ 前缀发布。它不是这些应用的官方团队——Radarr 是 Radarr 团队写的,linuxserver 只是“搬运工 + 标准化包装”。类似的团队还有 hotio,覆盖范围更聚焦在 *arr 和媒体服务。

他们的选品有明显边界:主力是影音媒体(*arr 全家桶、Plex、Jellyfin)、下载器、配套工具,只零星涉及基础设施(如 linuxserver/mariadb)。Redis、Elasticsearch、MySQL 这类核心组件他们不碰——这些软件的官方镜像已经足够好,第三方重新打包没有增值空间。LinuxServer 的增值恰恰在“上游没有好镜像”的领域。

与官方镜像的具体区别:

维度官方镜像LinuxServer 镜像
打包者软件作者或 Docker Library 团队第三方社区团队
权限模型各自为政,很多固定 UID 或 root统一 PUID/PGID,挂载文件权限不乱
配置路径每家不同统一 /config
init 系统通常直接跑应用进程s6-overlay,带 custom-cont-init.d 扩展钩子
架构支持核心软件一般齐全冷门应用也覆盖 ARM64/ARMv7,树莓派友好
更新节奏跟随上游发版CI 每周重建,带最新基础系统安全补丁
镜像体积通常精简偏大(多一层 init 和工具)

代价是中间多了一层:他们的基础镜像改版会引入自己的坑——之前修复硬链接时遇到的 custom-cont-init.d 目录从 /config 挪到容器根,就是 linuxserver 基础镜像变更造成的,与 Radarr 本身无关。

经验法则:基础设施用官方镜像,homelab 应用用 linuxserver/hotio

6. 统一入口的三次演进:从裸端口到 Homepage 占根

服务补全之后,剩下最影响日常体验的问题是入口。这个问题我前后折腾了三轮,每一轮都解决了一点、也暴露了一点,值得完整记录下来。

6.1 阶段一:裸端口,记不住

最初每个服务一个端口,访问全靠 http://192.168.1.7:<端口>

服务端口服务端口
Radarr7878Bazarr6767
Jackett9117ChineseSubFinder19035
qBittorrent8085AdGuard Home8080
Jellyfin8096Jellyseerr5055

八九个端口,时间长了根本记不住,每次想打开某个页面都要先翻备忘录。

6.2 阶段二:Caddy 子路径收敛

第一反应是上反向代理,把所有服务收敛到 raspberrypi.local/<服务名>

为什么选 Caddy 而不是 Nginx:两者都能做反代,但家庭场景下 Caddy 的 Caddyfile 三五行就搞定,还能在 .local 私有域名上自动生成本地证书、自动处理 HTTPS 重定向;Nginx 要写 server/location 块、自己搞自签证书。功能和生态 Nginx 无疑更强,但树莓派局域网反代这个需求,Caddy 省掉很多模板代码。

为什么选子路径而不是子域名,核心原因是 mDNS。树莓派默认运行 Avahi,实现了 mDNS:同一个局域网里,设备不需要任何 DNS 配置就能通过 raspberrypi.local 访问到树莓派。但 radarr.raspberrypi.local 这种子域名不会被 mDNS 自动解析,必须依赖真正的 DNS 服务器——要么每台设备改 DNS 指向 AdGuard Home,要么改路由器 DHCP,都引入了额外的网络层配置,一旦 DNS 出问题整个局域网受影响。子路径方案只依赖 raspberrypi.local 这一个主机名,零网络配置,开箱即用

raspberrypi.local {
    reverse_proxy /radarr* radarr:7878
    reverse_proxy /jackett* jackett:9117
    reverse_proxy /bazarr* bazarr:6767
    reverse_proxy /adguard* adguardhome:80
}

但子路径有个硬伤:后端服务必须支持 URL Base。否则页面 HTML 里的 JS/CSS/API 链接会指向根路径 /,直接白屏或 404。*arr 家族都支持(Radarr 的 UrlBase、Jackett 的 BasePathOverride、Bazarr 的 base_url),但有两个服务怎么都挂不上:

  • qBittorrent:WebUI 没有 URL Base 选项,前端代码写死了根路径 /api/v2/...,社区呼吁多年官方始终没加;
  • ChineseSubFinder:作者转向 Lite 路线后 WebUI 基本不维护,同样按根路径部署。

于是这两个只能继续用原端口访问,方案天然残缺。

6.3 阶段三:想通了——入口只需要一个

后来装了 Homepage 导航页:一份 YAML 把所有服务收进一个首页,还能挂 docker socket 显示容器运行状态、探测服务存活:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  homepage:
    image: ghcr.io/gethomepage/homepage:latest
    container_name: homepage
    environment:
      - PUID=1000
      - PGID=100
      - TZ=Asia/Shanghai
      # v1+ 必须显式允许访问的 Host
      - HOMEPAGE_ALLOWED_HOSTS=raspberrypi.local,192.168.1.7:3001,raspberrypi.local:3001
    volumes:
      - /home/pi/docker/homepage/config:/app/config
      # 只读挂 docker socket,用于首页显示容器运行状态
      - /var/run/docker.sock:/var/run/docker.sock:ro
    ports:
      - "3001:3000"
    restart: unless-stopped

这个小服务踩了两个坑:ghcr.io 直连拉不动(Docker Hub 的加速手段对它无效,解法是走南京大学 ghcr 镜像站拉取后 docker tag 回原名);v1 强制 Host 校验(不配 HOMEPAGE_ALLOWED_HOSTS 时页面能开但数据接口全部报 Host validation failed,必须把访问用的所有主机名都列进去)。

Homepage 用着用着,一个念头冒了出来:既然所有服务都能从导航页点过去,为什么还要给每个服务维护一条 Caddy 子路径? 入口只需要记住一个,子路径反代解决的是“记住 N 个地址”的问题,而 Homepage 已经把这个问题解决掉了。

那顺手把 Homepage 也挂到 Caddy 的 /homepage 子路径下,不就整齐了?一查才发现行不通,而且很讽刺:Homepage 恰恰是全栈里最不适合挂子路径的服务。它是 Next.js 应用,官方对子路径的支持一直很勉强——早年在 discussion #150 里加过一个 settings.yamlbase 设置(往 HTML 里插 <base href>),需要代理端剥掉前缀配合,但 JS 资源、/api 接口、widget 数据在子路径下依然经常出问题,到 2025 年的 discussion #5087 里维护者的建议仍然是“把根路径直接给 dashboard 用”。再叠上 v1 的 Host 校验,这会是最痛苦的一个子路径——而它偏偏是入口,它挂了什么都点不了。

于是有了最终方案,简单到有点不好意思:Homepage 直接占 raspberrypi.local 的根路径,其他服务全部直连端口,从导航页点过去

raspberrypi.local {
    # 根路径直接给 Homepage 导航页,其他服务从导航页点过去、直连端口
    reverse_proxy homepage:3000
}

# 唯一例外:Vaultwarden 强制要求 HTTPS(见 4.3),单独开一个 HTTPS 端口
raspberrypi.local:8443 {
    reverse_proxy vaultwarden:80
}

Homepage 的服务清单里直接写端口链接(mDNS 下主机名比 IP 好写):

1
2
3
4
5
6
7
- 影音:
    - Jellyfin:
        href: http://raspberrypi.local:8096
        description: 媒体播放
    - Jellyseerr:
        href: http://raspberrypi.local:5055
        description: 点播入口

收尾清理:删掉 Caddyfile 里所有子路径反代;把阶段二给后端配的 URL Base 全部还原(Radarr 的 UrlBase 清空、Jackett 的 BasePathOverride 改回 null、Bazarr 的两个 base_url 改回 /),否则直连端口时还会被重定向到 /radarr 这种子路径;HOMEPAGE_ALLOWED_HOSTS 里加上不带端口的 raspberrypi.local(经 Caddy 443 访问时 Host 不带端口)。

flowchart LR
    User[浏览器 / 手机] -->|mDNS 自动发现| Pi[raspberrypi.local]
    Pi --> Caddy[Caddy 443/8443]
    Caddy -->|根路径 443| HP[Homepage 3001]
    Caddy -->|8443 HTTPS| VW[Vaultwarden]
    HP -.->|点卡片直连端口| Svc[Jellyfin :8096<br>Jellyseerr :5055<br>Radarr :7878<br>qBittorrent :8085<br>...]
    style HP fill:#fff3bf
    style VW fill:#fff3bf

实线是经过 Caddy 的两跳,虚线是 Homepage 卡片指向的直连链接。qBittorrent、ChineseSubFinder 这些挂不上子路径的服务天然兼容——反正都是直连端口;Vaultwarden 因为强制 HTTPS 走了 Caddy 的 8443,是唯一例外。

6.4 三轮方案对比

方案要记几个地址网络配置前提条件结局
裸端口N 个 IP:端口记不住
Caddy 子路径1 个主机名 + N 个路径无(mDNS 白送)每个后端支持 URL BaseqBittorrent/CSF 挂不上
Homepage 占根1 个最终方案

回头看,子路径方案并不是白折腾:它让我确认了 mDNS 是局域网零配置的基石,也摸清了哪些服务支持 URL Base。但最终形态比它更简单——反代只服务入口本身,入口之内全是直连

7. 当前栈全景与下一步

至此树莓派上的服务全景:

分组服务端口
播放与点播Jellyfin、Jellyseerr8096、5055
下载管线Radarr、Jackett、qBittorrent、Bazarr、ChineseSubFinder7878、9117、8085、6767、19035
入口与管理Homepage、Vaultwarden、Caddy、Portainer3001(经 443 反代)、8443(HTTPS 反代)、80/443、9443
网络AdGuard Home、V2Ray53/3000、10808/10809

日常唯一需要记的地址是 https://raspberrypi.local/(首次访问接受自签证书),其余全部从导航页点过去。

如果继续折腾,候选清单(都 ARM64 友好):

  • Sonarr:电视剧版 Radarr,自动追更按季整理;
  • Prowlarr:Jackett 的现代化替代,索引器统一管理并同步给所有 *arr;
  • Recyclarr:把 TRaSH Guides 推荐的画质配置自动同步进 Radarr/Sonarr;
  • Navidrome:音乐流媒体,私人 Spotify;
  • Immich:自托管 Google Photos,8G 内存能跑;
  • Uptime Kuma / Dozzle:服务存活监控 / 浏览器里看容器日志。

不建议在树莓派上碰的:Tdarr(转码农场,算力不够)、Nextcloud(能跑但体验重)。

本文由作者按照 CC BY 4.0 进行授权