VPS 架設 Jellyfin 教學:Docker 安裝與轉碼優化
本指南教您使用 Docker 在 VPS 部署 Jellyfin,並解決常見的 file permissions 讀取失敗與 CPU transcoding 效能瓶頸。透過設定 block-storage 與優化 Direct play 策略,讓您在不具備 GPU 的主機上也能順暢串流個人媒體庫。
建立目標
在 VPS 上架設 Jellyfin 媒體伺服器:包含一個 container、三個 volumes,以及一個存放電影與影集的 block-storage disk,可透過任何瀏覽器或 Jellyfin app 存取。安裝過程僅需一個 15 行的 compose file。後續發生的問題主要源於兩點:container 無法讀取的 file permissions,以及要求不具備 GPU 的 VPS 進行不必要的 video transcoding。本指南將重點放在這兩點,因為這是最常收到支援請求的地方。
Jellyfin 為完全免費且開源的軟體,不需建立帳號、無付費功能,且不收集 telemetry —— 這也是它幾乎出現在每一份 2026 年值得 self-hosting 的項目 清單中的原因。它僅播放您擁有的媒體內容。本指南不提供任何內容,也不討論如何獲取內容。
在租用任何服務前的轉碼現實
請先閱讀此內容,因為這會影響您的購買決策。按下播放鍵時,媒體伺服器會執行以下兩種操作之一。Direct play 會直接串流原始檔案:VPS 從磁碟讀取位元組並傳送,幾乎不消耗 CPU。Transcoding 會即時重新編碼影片(例如更改解析度、編碼格式或燒錄字幕),這完全依賴 CPU 運算。
典型的 VPS 沒有 GPU。因此,每次轉碼都會使用 libx264/libx265 透過 CPU 執行,軟體編碼的成本極高。單個 1080p H.264 轉碼任務就可能佔滿數個共享 vCPU;4K 或 HEVC 轉碼通常無法達到即時處理速度,導致播放卡頓並持續緩衝。硬體轉碼(在配備 Intel iGPU 或 Nvidia 卡的家用設備上成本很低)在 VPS 上通常無法使用,除非您的供應商提供 GPU 實例。
因此,在 VPS 上的核心策略是:避免轉碼。請將您的媒體庫儲存為客戶端可原生播放的編碼格式——例如 H.264 影片、AAC 或 AC3 音訊,並使用 MP4 或 MKV 封裝——同時選擇支援 direct-play 的客戶端應用程式:例如 Android TV、iOS 和 Roku 的原生 Jellyfin App,以及 Infuse、Kodi 和桌面版 Jellyfin Media Player。若能做到這一點,VPS 就不需要執行 ffmpeg,一台配置適中的 2 vCPU 主機即可同時為多人提供串流服務。若計畫進行轉碼,您需要更大型且昂貴的主機,即便如此,處理 4K 影片仍具備高度風險。
同時也要計算頻寬,這是另一個容易被忽略的因素。Direct play 會以檔案本身的位元率進行傳送。一個壓縮過的 1080p 檔案位元率約為 8-12 Mbps;1080p Blu-ray remux 約為 20-30 Mbps;4K HDR 約為 40-80 Mbps。若有三個人同時 direct-play 10 Mbps 的檔案,您的 VPS 就需要維持 30 Mbps 的持續上傳頻寬。請檢查您方案中的兩個數據:埠口速度(是否能提供 30 Mbps 的上行頻寬?)以及每月流量限制。一部兩小時且位元率為 10 Mbps 的電影約消耗 9 GB 流量,因此每月 1 TB 的流量配額僅能播放約一百部此類電影(平均每天三、四部);若家庭成員觀看 4K 影片,其位元率高出四到八倍,流量消耗速度會快得多。
Prerequisites
- 一台全新的 Ubuntu 24.04 KVM VPS,具備 root 或 sudo 權限,並已安裝 Docker 與 Compose plugin。
- 用於存放媒體檔案的 block-storage 磁碟卷,容量須足以容納您的媒體庫(參閱下方的容量建議)。請勿將影片存放在 VPS 預設的微型 root disk 中。
- 若需透過公網 HTTPS 存取,請準備一個網域名稱;若希望保持完全私密,請使用 同台 VPS 上的 WireGuard VPN。
- 您擁有合法串流權限的媒體檔案 — 例如您自行轉錄的內容、自行錄製的內容或您擁有的檔案。
首先掛載 block storage
請先在供應商的控制台掛載 volume,接著尋找並掛載它。從 lsblk 取得裝置名稱 —— 名稱會類似 /dev/sdb 或 /dev/vdb,絕不會是 root disk。
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 至關重要:若缺少此設定,當 block volume 被卸除時,系統將無法正常開機並進入 emergency shell。最嚴重的錯誤是在已有資料的 volume 上執行 mkfs.ext4 —— 這會清除所有資料。請僅對新 volume 進行格式化;若磁碟中已有您的 library,請直接跳至 fstab 設定步驟。
依照 Jellyfin 預期的格式配置媒體檔案
Jellyfin 透過資料夾與檔案名稱來比對 metadata。若檔案結構錯誤,電影會顯示為無標題且無海報的檔案,或導致集數與錯誤的影集比對。請遵循以下三項規則:每部電影必須存放於獨立的 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 中特定內容類型的 library,混合存放會導致 metadata 提供者判斷錯誤。
Permissions: 導致媒體庫顯示為空的最主要原因
這是一個常讓使用者浪費時間的誤解。官方的 jellyfin/jellyfin 映像檔不支援 PUID/PGID 環境變數,這些變數屬於 LinuxServer.io 的映像檔 (lscr.io/linuxserver/jellyfin)。在官方映像檔中,您必須透過 compose 中的 user: 鍵值來控制使用者;若未設定,容器將以 root 身分執行。無論使用哪種映像檔,規則皆相同:容器執行時的 uid/gid 必須具備讀取並遍歷所有媒體目錄的權限。
我們將以 uid/gid 1000 執行(這是標準 Ubuntu 系統中第一個非 root 使用者)。請確認您的設定並設定權限:
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目錄需要具備執行權限(即 755 中的 x),而不僅僅是讀取權限。若缺少執行權限,即使容器可以列出檔案名稱,也無法進入該資料夾。導致整個媒體庫消失的陷阱通常在於父目錄:如果容器的 uid 無法遍歷掛載點本身,它就無法到達 /media/Movies 或 /media/Shows,導致所有媒體庫同時顯示為空,並在日誌中出現 Access to the path ... is denied。任何無法讀取的單一媒體資料夾都會被記錄並跳過,因此以 root 身分複製的一批檔案會從媒體庫中靜默消失。這就是為什麼我們必須遞迴執行 chown 並為每個目錄設定執行權限,而不是僅修正單一資料夾。
The docker-compose file
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,因此不需要對媒體庫進行寫入;唯讀模式可防止檔案因誤刪或錯誤的 plugin 而毀損。連接埠刻意綁定至 127.0.0.1 — 由於 Jellyfin 的網頁登入使用 plain HTTP,因此我們絕不將 8096 埠對公網開放。JELLYFIN_PublishedServerUrl 是伺服器用於區域網路自動偵測的位址 — 這是透過 LAN UDP 廣播進行,因此來自網際網路的用戶端不會看到它,僅能使用你在 App 中輸入的 URL。請將其設定為應告知用戶端的位址,並預期在遠端裝置上需手動輸入該 URL。
在 compose 目錄下啟動服務:
docker compose up -d
docker logs -f jellyfin首次執行:設定精靈與媒體庫
由於連接埠已綁定至 localhost,請透過筆記型電腦的 SSH tunnel 連線至精靈,而非開啟防火牆孔洞:
ssh -L 8096:127.0.0.1:8096 you@your-vps-ip接著瀏覽至 http://localhost:8096。精靈會引導您完成語言設定,接著建立一個具備強密碼的 admin 使用者 — 此帳戶即為您的伺服器,請勿使用暫時性的密碼。新增第一個媒體庫:選擇內容類型 Movies,並將路徑指向 /media/Movies(此為容器內部路徑,而非主機路徑),接著針對 Shows 重複上述步驟並指向 /media/Shows。完成後,Jellyfin 會開始掃描。若媒體庫規模較小,應會在 1 到 2 分鐘內顯示海報與標題。後續可透過 Dashboard → Libraries 新增或編輯媒體庫,並使用 Scan All Libraries 強制重新掃描。
若您需要使用轉碼功能,請開啟 Dashboard → Playback → Transcoding,並將轉碼暫存路徑設定為 /cache/transcodes,以確保轉碼產生的暫存檔會寫入快取磁碟,而不會撐爆 /config。請將硬體加速設定為 None — 因為系統中沒有可用的 GPU。
遠端存取:使用 TLS 反向代理,或維持使用 VPN
您有兩種安全的外部存取 Jellyfin 方法,以及一種應避免的不安全方法。不安全的方法是直接將 port 8096 對外公開:登入資訊將以明文傳輸,且該 port 會在數小時內遭到暴力破解。
選項 A — TLS 反向代理。 將 Jellyfin 部署於子網域下,並透過 Traefik (具備 Docker 應用程式自動 TLS 功能) 或透過 nginx 並搭配 由 Certbot 核發的 Let's Encrypt 憑證。Jellyfin 使用 WebSockets 進行即時更新,因此代理伺服器必須轉發 upgrade headers。Traefik 會自動處理此動作;nginx 則需要明確設定,且必須對 upstream 使用 HTTP/1.1,否則 upgrade 動作將無法執行:
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,以便在觀眾發現前先得知停機狀態。
選項 B — 維持 VPN 私有連線。 完全不要公開 8096 port;僅透過終止於同一台主機的 WireGuard tunnel 存取 Jellyfin。對於家庭環境,這是最簡單的安全選擇 —— 無需憑證、無公開暴露、無暴力破解攻擊面。將 container 綁定至 tunnel 位址或 localhost,並透過 VPN 連線。關於 tunnel 本身的設定,請參閱 用於私有 VPS 的 WireGuard VPN 設定。
儲存空間規劃與備份
請根據檔案大小而非檔案數量來預算空間。壓縮過的 1080p 電影每個約 4-15 GB;1080p remux 約 20-40 GB;一季 1080p 電視影集約 15-40 GB;任何 4K 內容每個約 40-100 GB。若包含數百部電影與部分影集,建議配置 2-4 TB 的磁碟區;一次性預留充足的 block volume 空間,比日後進行資料遷移更划算。
/config 包含完整的伺服器狀態,是唯一必須備份的項目。請使用 snapshot 或 stop-and-tar 方式進行備份,並將副本儲存在主機之外:
docker compose down
sudo tar czf jellyfin-config-$(date +%F).tgz -C ~/jellyfin config
docker compose up -d/cache 與 transcode 資料夾為可捨棄項目。位於 /mnt/media 的媒體檔案應另外備份,或接受重新轉檔(re-rippable)——考量到檔案大小,多數人會選擇後者。版本升級屬於 docker compose pull && docker compose up -d 範疇;上述的 :10 標籤會維持在 10.x major 版本內,因此遷移至下一個 major 版本需手動編輯標籤。在進行操作前,請先閱讀 Jellyfin 的 release notes,因為 library schema 遷移會在 major 版本更迭時發生。
Failure modes, with the strings you will see
掃描後 Library 為空。 Dashboard → Logs (或 ~/jellyfin/config/log/log_*.log) 中的紀錄顯示:
System.UnauthorizedAccessException: Access to the path '/media/Movies' is denied.容器的 uid 無法讀取該路徑。原因:媒體檔案由 root 或非 user: 設定值的 uid 擁有,或目錄缺少 execute bit,或父層 mount 對該 uid 不可遍歷。修復方法:chown -R 1000:1000 /mnt/media,目錄 755,檔案 644,然後重新掃描。
Playback 導致 CPU 滿載與緩衝。 docker stats jellyfin 顯示 CPU 使用率接近核心數的 100%,且 Dashboard → Playback 將該工作階段列為 Transcode,且速度低於 1.0x。客戶端未進行 direct-play,導致 VPS 進行 CPU-transcoding 且速度慢於實時,進而造成緩衝。原因:不支援的 codec 或 container,字幕 burn-in,或 HDR tone-mapping。修復方法:切換至支援 direct-play 的客戶端,將來源保持在 H.264/AAC,使用 text 字幕 (SRT) 而非會強制 burn-in 的 image 字幕 (PGS/VOBSUB),且避免在僅具備 CPU 的設備上播放 4K HDR。
"No compatible streams are available." 完整訊息通常為 "This client isn't compatible with the media and the server isn't sending a compatible media format."。客戶端拒絕來源檔案,且 fallback transcode 也無法啟動。原因:ffmpeg 指令錯誤、檔案無法讀取,或使用者設定檔阻擋了影片轉換。修復方法:閱讀 Dashboard → Logs 中的 ffmpeg 行,確認檔案是否可播放,若依賴 transcoding,請檢查使用者的播放權限,並嘗試使用另一個客戶端以排除瀏覽器 codec 問題。
Films 沒有海報或海報錯誤。 Metadata 不匹配。原因:電影未放在其專屬的 Name (Year) 資料夾中,季資料夾名稱為 S01 而非 Season 01,集數未採用 S01E01 格式,或缺少年份。修復方法:依照上述佈局重新命名,然後執行 Refresh metadata → Replace all,或對單一項目使用 Identify 以鎖定正確的 TMDB/TVDB 項目。
FAQ
沒有 GPU 的 VPS 可以進行影片轉碼嗎?
可以,但只能使用 CPU 進行,且成本較高。單一 1080p 軟體轉碼可能會佔滿數個 vCPU;4K 或 HEVC 影片通常無法達到即時轉碼,導致播放緩衝。最佳做法是避免轉碼:將媒體庫維持在 H.264/AAC 格式,並使用支援 direct-play 的用戶端應用程式,讓 VPS 僅負責傳輸 bytes。僅在確實需要即時轉碼時,才租用帶有 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 reverse proxy 後方以加密登入與串流,並安裝 fail2ban;切勿直接暴露 port 8096,因為這會以明文傳送密碼。方案二:將其完全私有化,僅透過 VPN 存取,這是家庭環境最簡單且安全的選擇。請直接將公網位址提供給應用程式;由於自動發現 (autodiscovery) 是透過區域網路廣播,因此無法傳達給經由網際網路連線的用戶端。
Jellyfin VPS 需要多少磁碟空間與頻寬?
磁碟需求取決於品質:壓縮過的 1080p 電影預計每部 4-15 GB,remux 影片每部 20-40 GB,4K 影片則需 40-100 GB,因此大多數媒體庫需要 2-4 TB 的 block volume。頻寬則由 direct-play 的位元率決定:1080p 串流約需 8-12 Mbps,4K 則需要更多;請確認您的 port 速度足以應付同時在線的觀看人數,並留意每月流量限制。若計畫進行轉碼,請預留 CPU 餘裕;若計畫使用 direct-play,請優先考慮頻寬而非核心數。
在 VPS 上執行 Jellyfin 是否合法?
Jellyfin 本身是免費的開源軟體,執行它是完全合法的。重點在於內容:請僅串流您擁有或擁有授權的媒體,例如您自己的光碟轉錄檔、錄影檔或您有權使用的檔案。Jellyfin 不附帶任何媒體,也不提供獲取媒體的管道;它僅是您既有媒體庫的播放器。