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 VPS 也能同时为多名用户提供流媒体服务。如果计划进行转码,就需要更大、更昂贵的 VPS;即使如此,4K 转码仍然不是理想选择。
还要计算带宽,因为这是另一个容易被忽略的问题。直接播放会按文件本身的码率发送数据。压缩后的 1080p 文件通常需要 8-12 Mbps;1080p Blu-ray remux 需要 20-30 Mbps;4K HDR 需要 40-80 Mbps。3 个人同时直接播放 10 Mbps 的文件,会持续占用 VPS 30 Mbps 的上行带宽。检查套餐中的两个数值:端口速率(上行是否能达到 30 Mbps)和每月流量上限。一个时长为两小时、码率为 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 很重要:如果没有它,块存储卷一旦被卸载,服务器将无法启动并进入紧急 shell。这里最常见的严重错误,是对已包含数据的卷运行 mkfs.ext4,这会清除其中的数据。仅格式化新卷;如果磁盘中已经存有库数据,请直接跳到 fstab 配置行。
按 Jellyfin 的要求整理媒体
Jellyfin 根据文件夹名和文件名匹配元数据。目录结构错误时,电影可能会显示为没有标题且没有海报的文件,剧集也可能匹配到错误的系列。规则只有三条:每部电影都放在独立的 Name (Year) 文件夹中,并使用与文件夹匹配的文件名;季文件夹命名为 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:10
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逐行说明:user: "1000:1000" 实际设置了文件权限,并与上面的所有权配置匹配。/config 保存整个服务器、账户、库、元数据和监控状态,因此必须可写,也是需要备份的内容。/cache 是可丢弃的工作空间。媒体挂载点使用 :ro(只读)是有意为之:Jellyfin 默认将封面和元数据存储在 /config 下,因此不需要写入媒体库;只读设置还可防止误删文件或有问题的插件修改文件。端口有意绑定到 127.0.0.1。Jellyfin 的 Web 登录使用普通 HTTP,因此绝不将 8096 发布到公网。JELLYFIN_PublishedServerUrl 是服务器用于本地自动发现的通告地址,即 LAN 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 在响应;open-kritt 会从同一 VPS 上的容器中运行扫描。
选项 B,通过 VPN 保持私有。 完全不要发布 8096;只能通过终止在同一台服务器上的 WireGuard 隧道访问 Jellyfin。对于家庭使用,这是最简单的安全选择:无需证书,不暴露公网,也没有暴力破解面。将容器绑定到隧道地址或 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;上面的 :10标签仍在 10.x 主版本范围内,因此升级到下一个主版本需要有意修改标签。修改前请快速查看 Jellyfin 的发行说明,因为主版本升级会执行媒体库架构迁移。固定标签加上一个已备份的状态目录,就是所有常驻容器的完整方案。这也是 让自托管代理在重启后保留记忆和计划任务 所采用的模式。
故障模式及对应提示信息
扫描后媒体库为空。 Dashboard → Logs(或 ~/jellyfin/config/log/log_*.log)中的日志显示:
System.UnauthorizedAccessException: Access to the path '/media/Movies' is denied.容器的 uid 无法读取该路径。原因可能是:媒体归 root 或其他 uid 所有,而不是归 user: 值对应的 uid 所有;目录缺少执行权限位;或者父级挂载点本身无法由该 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),并且完全不要在仅使用 CPU 的设备上播放 4K HDR。
“没有可用的兼容流。” 完整提示通常是 “This client isn't compatible with the media and the server isn't sending a compatible media format.” 客户端拒绝了源文件,备用转码也未能启动。原因可能是 ffmpeg 命令错误、文件不可读,或用户配置禁止视频转换。修复方法:在 Dashboard → Logs 中查看 ffmpeg 命令行,确认文件本身可以播放;如果依赖转码,请检查用户的播放权限;再尝试第二个客户端,以排除浏览器编解码器兼容性问题。
影片没有海报或海报错误。 元数据匹配失败。原因可能是:影片未放在独立的 Name (Year) 文件夹中;季文件夹命名为 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 不提供任何媒体,也不提供获取媒体的方式;它只是用于播放您已经拥有的媒体库。