VPS 部署 Jellyfin:自建媒体库流媒体播放
用 Docker 在 VPS 上部署 Jellyfin,配置块存储、媒体权限和安全远程访问。了解直接播放与 CPU 转码的区别,以及 4K HEVC 转码为何容易持续缓冲。
构建内容
在 VPS 上部署 Jellyfin 媒体服务器:使用一个容器和三个卷,并通过一块保存电影和剧集的块存储磁盘提供服务。您可以从任何浏览器或 Jellyfin 应用访问它。安装只需一个包含 15 行内容的 compose 文件。之后出现的问题主要来自两个方面:容器无权读取文件,以及要求没有 GPU 的 VPS 转码它不适合转码的视频。本指南的大部分篇幅都用于说明这两个问题,因为支持请求主要集中在这里。
Jellyfin 免费且完全开源,不需要账户,不包含付费功能,也不收集遥测数据。因此,它几乎出现在所有 2026 年值得自行托管的服务列表中。它可以播放您拥有的媒体内容。Jellyfin 不提供任何内容,本指南也不涉及获取内容。
租用之前,先了解转码的实际情况
先阅读本节,因为它会影响你的选购决策。按下播放后,媒体服务器通常会执行两种操作之一。直接播放会原样传输文件:VPS 从磁盘读取字节并通过网络发送,几乎不占用 CPU。转码会实时重新编码视频,例如更改分辨率、编码格式,或将字幕烧录到画面中。这完全依赖 CPU。
典型 VPS 没有 GPU。因此,每次视频转码都由 CPU 配合 libx264/libx265 完成,而软件编码的开销很高。单个 1080p H.264 转码就可能占满多个共享 vCPU;4K 或 HEVC 转码通常完全无法达到实时速度,播放会停顿并持续缓冲。硬件转码可以让配备 Intel iGPU 或 Nvidia 显卡的家用设备以较低成本完成这些工作,但除非服务商提供 GPU 实例,否则 VPS 通常无法使用。
因此,在 VPS 上的整体策略是:避免转码。 将媒体库保存为客户端可以原生播放的编码格式,例如 H.264 视频、AAC 或 AC3 音频,并使用 MP4 或 MKV 容器;同时选择支持直接播放的客户端应用,例如适用于 Android TV、iOS 和 Roku 的原生 Jellyfin 应用,以及 Infuse、Kodi 和桌面版 Jellyfin Media Player。这样,VPS 在播放期间无需运行 ffmpeg 转码,配置适中的 2 vCPU 实例也可以同时为多名用户提供流媒体服务。如果计划进行转码,就需要更大且更昂贵的实例,即使如此,4K 转码仍然不适合作为可靠方案。
还要计算带宽,因为这也是容易被忽略的问题。直接播放会按照文件自身的码率发送数据。压缩后的 1080p 文件通常为 8-12 Mbps;1080p Blu-ray 重封装文件通常为 20-30 Mbps;4K HDR 通常为 40-80 Mbps。3 名用户同时直接播放码率为 10 Mbps 的文件时,VPS 需要持续提供 30 Mbps 的上行带宽。检查套餐中的两个数值:端口速率(上行是否能达到 30 Mbps)和每月传输流量上限。一部时长为 2 小时、码率为 10 Mbps 的电影,上传流量约为 9 GB。因此,按流量计费且每月限额为 1 TB 的套餐,每月只能支持略多于 100 部此类电影,即每天 3 或 4 部;而家庭观看 4K 内容时,码率是其 4 到 8 倍,流量消耗会快得多。还要将同一实例产生的其他出站网络流量计入同一预算,包括 自托管的 RustDesk 中继;当两个对等端无法直接连接时,该中继会承载完整的远程桌面会话。
前置条件
- 一台全新的 Ubuntu 24.04 KVM VPS,具有 root 或 sudo 权限,并已安装 Docker 和 Compose plugin。
- 一个用于存储媒体文件的块存储卷,容量应根据媒体库大小确定(见下文的容量规划)。VPS 附带的小型根磁盘不适合存放电影。
- 如果需要通过公网使用 HTTPS,则需要一个域名;如果希望整个服务保持私有,则需要在同一 VPS 上配置 WireGuard VPN。
- 您必须依法拥有流媒体播放权的媒体,包括您自己的翻录文件、录制内容和自有文件。
先挂载块存储
在云服务提供商的控制面板中挂载卷,然后识别并挂载它。使用 lsblk 获取设备名称,结果通常类似 /dev/sdb 或 /dev/vdb,但绝不能是根磁盘。
lsblk
sudo mkfs.ext4 /dev/sdb # ONLY on a new, empty volume: this ERASES it
sudo mkdir -p /mnt/media
sudo blkid /dev/sdb # copy the UUID shown for this device使用 UUID 挂载,而不是使用 /dev/sdb,因为设备字母可能在重启后重新分配,最终可能格式化或挂载错误的磁盘。在 /etc/fstab 中添加一行:
UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /mnt/media ext4 defaults,nofail 0 2sudo mount -a
df -h /mnt/medianofail 很重要:如果没有它,块卷一旦被卸载,服务器就会拒绝启动并进入 emergency shell。这里最常见的错误是在已经包含数据的卷上运行 mkfs.ext4,这会清除卷中的数据。仅格式化新卷;如果磁盘中已经存放了您的库文件,请直接添加 fstab 行。
按 Jellyfin 的要求整理媒体
Jellyfin 会根据文件夹和文件名匹配元数据。目录结构不正确时,电影可能会显示为没有标题且没有海报的文件,剧集也可能匹配到错误的系列。以下 4 条约定基本可以解决所有问题:每部电影都放在独立文件夹中,并使用匹配的文件名,命名为 Name (Year)(Jellyfin 将年份视为可选项,但添加年份可以显著提高匹配可靠性);季文件夹命名为 Season 01,不要使用 S01;剧集文件使用 S01E01;特别篇放在 Season 00 中。
/mnt/media
├── Movies
│ ├── Blade Runner (1982)
│ │ └── Blade Runner (1982).mkv
│ └── Arrival (2016)
│ └── Arrival (2016).mkv
└── Shows
└── Severance (2022)
├── Season 01
│ ├── Severance - S01E01.mkv
│ └── Severance - S01E02.mkv
└── Season 00
└── Severance - The Lexington Letter.mkv电影名称中的 (Year) 不是装饰信息,而是用于区分翻拍版本,确保匹配器选择正确的标题。将 Movies 和 Shows 保持为独立的顶级文件夹,因为它们会分别成为特定内容类型的 Jellyfin 媒体库。混合存放会导致元数据提供程序匹配错误。Jellyfin 也可以正常索引第三个照片文件夹,但与专用照片服务器相比,功能较为有限。因此,如果相册对您很重要,请使用单独的设备运行 PhotoPrism 或 Immich,让这台服务器专门用于电影和电视节目。
权限:媒体库为空的首要原因
这是最容易让人排查一晚的误解。官方 jellyfin/jellyfin 镜像不会识别 PUID/PGID 环境变量,这些变量属于 LinuxServer.io 镜像(lscr.io/linuxserver/jellyfin)。在官方镜像中,您需要在 compose 中使用 user: 键控制容器用户;如果省略该键,容器将以 root 身份运行。无论使用哪种镜像,规则都相同:容器运行所使用的 uid/gid 必须能够读取并遍历每个媒体目录。
我们将使用 uid/gid 1000,这是标准 Ubuntu 系统上的第一个非 root 用户。请确认您的 uid/gid,并设置所有权:
id # confirm your user is uid=1000 gid=1000
sudo chown -R 1000:1000 /mnt/media
sudo find /mnt/media -type d -exec chmod 755 {} \;
sudo find /mnt/media -type f -exec chmod 644 {} \;
mkdir -p ~/jellyfin/config ~/jellyfin/cache
sudo chown -R 1000:1000 ~/jellyfin目录需要 execute 权限位(即 755 中的 x),不能只有读取权限。否则,容器虽然可以列出目录名称,却无法进入该目录。导致整个媒体库为空的陷阱在父目录:如果容器的 uid 无法遍历挂载点本身,它就无法到达 /media/Movies 或 /media/Shows;随后所有媒体库会同时显示为空,日志中则出现 Access to the path ... is denied。容器无法读取的任何单个媒体目录都会被记录并跳过。因此,以 root 身份复制的一批文件可能会悄无声息地从媒体库中消失。这就是我们要递归执行 chown,并为每个目录设置 execute 权限,而不是只修复某一个目录的原因。
docker-compose 文件
services:
jellyfin:
image: jellyfin/jellyfin:12
container_name: jellyfin
user: "1000:1000"
restart: unless-stopped
ports:
- "127.0.0.1:8096:8096"
volumes:
- ./config:/config
- ./cache:/cache
- /mnt/media:/media:ro
environment:
- JELLYFIN_PublishedServerUrl=https://jellyfin.example.com逐行说明:jellyfin/jellyfin:12跟踪当前的 12.x 发布线(截至 2026-10-10 为 12.2)。user: "1000:1000"设置 Jellyfin 进程运行时使用的 uid/gid。它不会更改任何文件的所有权或模式,因为 Docker 会在挂载目录中保留主机的 uid/gid。因此,上面的 chown 必须与其匹配。/config保存整个服务器的数据,包括账户、媒体库、元数据和观看状态,因此必须可写,也是需要备份的内容。/cache是可丢弃的工作空间。媒体挂载点为 :ro(只读),这是有意这样设置的:Jellyfin 默认将封面和元数据存储在 /config 下,因此无需写入媒体库;只读设置还可以防止误删文件或有问题的插件修改文件。端口明确绑定到 127.0.0.1。Jellyfin 的 Web 登录使用纯 HTTP,因此不会将 8096 发布到公网。JELLYFIN_PublishedServerUrl是服务器用于本地自动发现的地址,通过局域网 UDP 广播发送,因此互联网中的客户端不会看到它,只会使用您在应用中输入的 URL。将其设置为应告知客户端的地址,并准备在远程设备上手动输入该 URL。
在 compose 目录中启动服务:
docker compose up -d
docker logs -f jellyfin首次运行:安装向导和媒体库
由于该端口绑定到 localhost,请通过笔记本电脑上的 SSH 隧道访问安装向导,不要为此打开防火墙端口:
ssh -L 8096:127.0.0.1:8096 you@your-vps-ip现在访问 http://localhost:8096。安装向导会先引导您选择语言,然后创建一个使用强密码的管理员用户。该账户用于管理您的服务器,因此不要重复使用一次性密码。添加第一个媒体库:选择内容类型 Movies,将路径设置为 /media/Movies(这是容器内部的路径,不是主机路径),然后在 /media/Shows 处重复相同步骤并选择 Shows。完成后,Jellyfin 会开始扫描。对于较小的媒体库,正常情况下,海报和标题会在一两分钟内显示。之后可在 Dashboard → Libraries 下添加或编辑媒体库,并使用 Scan All Libraries 强制重新扫描。Jellyfin 获取的海报也使替代前端值得一试:基础功能正常后,您可以查看 Halcyon 使用这些海报将同一媒体库重建为可浏览的 90 年代音像店。
如果您需要进行任何转码,请打开 Dashboard → Playback → Transcoding,将转码临时路径设置为 /cache/transcodes,让转码产生的临时数据写入缓存卷,而不是使 /config 不断增大。将硬件加速保留为 None,因为没有 GPU 可用于加速。
远程访问:TLS 反向代理,或仅通过 VPN 访问
从外部访问 Jellyfin 有两种安全方式,也有一种应避免的不安全方式。不安全的方式是直接将 8096 端口发布到互联网:登录信息会以明文传输,而且该端口会在数小时内遭到暴力破解。
选项 A:TLS 反向代理。 将 Jellyfin 放在子域名后面,通过 为 Docker 应用自动配置 TLS 的 Traefik 代理,或使用 nginx,并配置 由 Certbot 签发的 Let's Encrypt 证书。Jellyfin 使用 WebSockets 提供实时更新,因此代理必须转发升级请求头。Traefik 会自动完成此操作;nginx 则必须显式配置这些请求头,并使用 HTTP/1.1 连接上游,否则升级请求不会生效:
location / {
proxy_pass http://127.0.0.1:8096;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}将 JELLYFIN_PublishedServerUrl 设置为 https:// 地址,使本地自动发现功能公布正确的 URL,让远程应用使用你提供的地址,并添加 fail2ban 以降低暴力破解尝试的频率,保护登录入口。服务器公开后,将 Uptime Kuma 指向该 URL,这样你会在观众发现服务中断之前收到通知。将其通知目标设置为 自托管的 ntfy 服务器,检查失败时会以手机推送的形式到达,而不是等到第二天早上才看到的电子邮件。将登录入口暴露到公网,也是审计服务器其余部分的合适时机,不要想当然地认为只有 443 端口在响应:sudo ss -tlnp 会列出所有监听端口及其对应进程。如果你还在服务器上部署自己编写的代码,open-kritt 会扫描代码仓库及其依赖项中的漏洞;它审计的是源代码,不是监听端口。
选项 B:通过 VPN 保持私有。 完全不要将 8096 发布到公网;只能通过终止在同一台服务器上的 WireGuard 隧道访问 Jellyfin。对于家庭使用,这是最简单的安全方案:不需要证书,不暴露公网,也没有暴力破解攻击面。compose 文件中的 localhost 绑定单独使用还不够,因为发布到 127.0.0.1 的端口只能从 VPS 本身访问,VPN 对等端无法访问。应改为在服务器的隧道地址上发布该端口(例如 "10.8.0.1:8096:8096",使用你自己的 WireGuard 服务器 IP),或者保持 localhost 绑定,并在前面配置一个监听隧道地址的代理,然后通过 VPN 连接。有关隧道本身,请参阅 为私有 VPS 设置 WireGuard VPN。
存储容量规划与备份
应根据画质而不是文件数量规划容量。压缩后的 1080p 电影每部约 4-15 GB;1080p remux 每部约 20-40 GB;一季 1080p 电视剧约 15-40 GB;4K 影片每部约 40-100 GB。几百部电影加上部分电视剧通常需要 2-4 TB 的卷。一次性为块存储卷预留更大的容量,比日后迁移更便宜。
/config 包含整个服务器状态,因此必须备份它。这可以通过创建快照,或停止服务后使用 tar 打包来完成,并将副本保存到服务器之外:
docker compose down
sudo tar czf jellyfin-config-$(date +%F).tgz -C ~/jellyfin config
docker compose up -d/cache 和转码目录都可以丢弃。对于 /mnt/media 中的媒体文件,可以单独备份,也可以接受日后重新抓取;考虑到所需空间,大多数人会选择后者。升级使用 docker compose pull && docker compose up -d;上面的 :12 标签仍处于 12.x 系列,因此迁移到下一个主版本时需要有意修改标签。修改前先浏览 Jellyfin 的发行说明,因为主版本会执行媒体库架构迁移。Jellyfin 在 12.0(2026 年 9 月)中取消了版本号开头的 “10.”:12.0 对应原本的 10.12,不存在 11.x,而 10.11.x 是旧版本方案的最后一个系列。如果较旧的 compose 文件仍写着 jellyfin/jellyfin:10,它就固定在 10.11(该标签最后一次移动是在 2026-06-06,指向 10.11.11),永远不会拉取 12.x。将其改为 :12 不只是修改标签:12.0 会更改数据库架构,并在首次启动时重写数据。因此应先停止服务器并创建上面的备份,因为这是唯一的回退方式。12.0 可直接从 10.10.7 或任何 10.11.x 版本升级,之后还需要完整扫描媒体库,耗时会明显长于平时。对于任何持续运行的容器,固定标签并备份一个状态目录就是完整方案;让自托管代理的记忆和计划在重启后继续保留,也是基于同一模式:让自托管代理的记忆和计划在重启后继续保留。
故障模式及对应提示信息
扫描后媒体库为空。 Dashboard → Logs(或 ~/jellyfin/config/log/log_*.log)中的日志显示:
System.UnauthorizedAccessException: Access to the path '/media/Movies' is denied.容器的 uid 无法读取该路径。原因可能是媒体文件归 root 或其他 uid 所有,而不是您的 user: 值;目录缺少执行权限;或者父级挂载点本身无法由该 uid 遍历。修复方法:执行 chown -R 1000:1000 /mnt/media,将目录权限设为 755,将文件权限设为 644,然后重新扫描。
播放占满 CPU 并持续缓冲。 docker stats jellyfin 显示 CPU 接近核心数乘以 100%,而 Dashboard → Playback 将会话列为 Transcode,速度低于 1.0x。客户端未进行直接播放,因此 VPS 正在以低于实时速度进行 CPU 转码,导致播放落后。原因可能是不受支持的编码器或容器、字幕烧录,或 HDR 色调映射。修复方法:改用支持直接播放的客户端;将源文件保留为 H.264/AAC;使用文本字幕(SRT),不要使用图像字幕(PGS/VOBSUB)。大多数 Jellyfin 客户端无法自行绘制图像字幕,因此会强制进行字幕烧录。纯 CPU 服务器应完全避免播放 4K HDR 内容。
“没有可用的兼容流。” 完整提示通常为 “此客户端不兼容该媒体,服务器也未发送兼容的媒体格式。” 客户端拒绝了源文件,备用转码也未能启动。原因可能是 ffmpeg 命令错误、文件无法读取,或用户配置禁止视频转换。修复方法:在 Dashboard → Logs 中查看 ffmpeg 命令行,确认文件本身可以播放;如果依赖转码,请检查用户的播放权限;再尝试第二个客户端,以排除浏览器编码器差异。
影片没有海报或海报错误。 元数据匹配失败。原因可能是文件夹或文件名与元数据提供商列出的标题不一致;翻拍作品或常见标题缺少年份,无法区分不同版本;季目录命名为 S01 而不是 Season 01;或者剧集未采用 S01E01 格式。修复方法:按照上文的布局重命名,然后选择 Refresh metadata → Replace all;也可以对单个项目使用 Identify,指定正确的 TMDB/TVDB 条目。
FAQ
VPS 可以不使用 GPU 转码视频吗?
可以,但只能使用 CPU,成本较高。单路 1080p 软件转码就可能占满多个 vCPU,而 4K 或 HEVC 通常无法跟上实时播放速度,因此播放会缓冲。更好的做法是避免转码:将媒体库保留为 H.264/AAC,并使用支持直接播放的客户端应用,这样 VPS 只需传输字节流。只有确实需要实时转码时,才应租用 GPU 实例。
扫描后 Jellyfin 媒体库为什么为空?
几乎总是权限问题。官方 jellyfin/jellyfin 镜像会以您设置的 user:(或 root)运行。如果文件对该 uid 不可读,扫描日志会记录 Access to the path ... is denied 并跳过这些文件。使用 chown -R 1000:1000 /mnt/media 修复所有权,为目录添加执行权限(755),然后重新扫描。同时检查父目录,因为如果容器的 uid 无法遍历 /mnt/media 本身,就无法访问媒体库目录,最终会显示为空。第二常见的原因是目录结构不符合 Jellyfin 的预期。
如何安全地远程访问 Jellyfin?
有两种可行方案。将其置于子域名上的 TLS 反向代理之后,以加密登录信息和媒体流,并添加 fail2ban。不要暴露明文端口 8096,因为该端口会以明文传输密码。或者让 Jellyfin 完全保持私有,只通过 VPN 访问,这是家庭环境中最简单的安全选择。应直接向应用提供公网地址;自动发现依赖本地网络广播,无法到达通过互联网接入的客户端。
Jellyfin VPS 需要多少磁盘空间和带宽?
磁盘需求取决于画质:每部压缩后的 1080p 影片预留 4-15 GB,每部 remux 预留 20-40 GB,4K 影片预留 40-100 GB。因此,大多数媒体库需要 2-4 TB 的块存储卷。带宽取决于直接播放的码率:每路 1080p 流需要 8-12 Mbps,4K 需要更多。请确认端口速率能够支持同时观看的用户数量,并监控每月传输流量上限。如果计划转码,应预留 CPU 余量;如果计划直接播放,应优先选择带宽,而不是更多 CPU 核心。
在 VPS 上运行 Jellyfin 合法吗?
Jellyfin 本身是免费的开源软件,运行它完全合法。关键在于内容:只能传输您拥有或获授权持有的媒体,例如自己的光盘抓取文件、录制内容,或您有权使用的文件。Jellyfin 不提供任何媒体,也不提供获取媒体的方式;它只是用于播放您已经拥有的媒体库。