Cách cài Jellyfin trên VPS dùng Docker
Hướng dẫn chạy Jellyfin trên VPS bằng Docker. Cách xử lý lỗi permission file và cấu hình để tránh CPU bị quá tải khi transcode video trên VPS không có GPU.
Những gì bạn đang xây dựng
Một Jellyfin media server trên VPS: một container, ba volume, và một ổ đĩa block-storage chứa phim và các chương trình truyền hình của bạn, có thể truy cập từ bất kỳ trình duyệt hoặc ứng dụng Jellyfin nào. Việc cài đặt chỉ tốn một file compose dài mười lăm dòng. Mọi lỗi phát sinh sau đó thường đến từ hai nguyên nhân — permission của file mà container không thể đọc, và việc yêu cầu một VPS không có GPU thực hiện transcode video — việc mà nó không nên làm. Hướng dẫn này dành phần lớn thời lượng để nói về hai vấn đề đó, vì đó là nơi các ticket hỗ trợ thường xuyên xuất hiện.
Jellyfin miễn phí và hoàn toàn open source, không yêu cầu tài khoản, không có tính năng thu phí, và không có telemetry — đó là lý do nó luôn nằm trong danh sách những thứ đáng để tự host vào năm 2026. Nó phát các media mà bạn sở hữu. Nó không cung cấp sẵn nội dung, và hướng dẫn này không nói về việc thu thập nội dung.
Thực tế về transcoding, trước khi bạn thuê bất cứ thứ gì
Hãy đọc kỹ phần này trước, vì nó sẽ thay đổi quyết định mua hàng của bạn. Một media server thực hiện một trong hai việc khi bạn nhấn play. Direct play truyền file nguyên bản: VPS đọc các byte từ disk và đẩy chúng qua mạng, tốn gần như không bao nhiêu CPU. Transcoding mã hóa lại video ngay lập tức — thay đổi độ phân giải, codec mới, hoặc burn-in phụ đề — và đó là công việc thuần túy của CPU.
Một VPS thông thường không có GPU. Vì vậy, mọi quá trình transcode đều chạy trên CPU với libx264/libx265, và software encoding rất tốn kém. Một lượt transcode 1080p H.264 duy nhất có thể làm quá tải vài vCPU dùng chung; một lượt transcode 4K hoặc HEVC thường không thể theo kịp thời gian thực, khiến việc phát video bị khựng và buffer mãi mãi. Hardware transcoding — thứ giúp việc này trở nên rẻ tiền trên một máy tại nhà có Intel iGPU hoặc card Nvidia — đơn giản là không khả dụng trừ khi nhà cung cấp của bạn cho thuê các instance có GPU.
Do đó, chiến lược trên VPS là: tránh transcoding. Hãy giữ thư viện của bạn ở các codec mà client có thể chơi natively — video H.264, audio AAC hoặc AC3, trong container MP4 hoặc MKV — và chọn các ứng dụng client hỗ trợ direct-play: các ứng dụng Jellyfin chính thức cho Android TV, iOS và Roku, cùng với Infuse, Kodi, và Jellyfin Media Player trên desktop. Làm như vậy, VPS sẽ không bao giờ phải chạm vào ffmpeg, và một máy 2 vCPU khiêm tốn có thể stream cho nhiều người cùng lúc. Nếu bạn có kế hoạch transcode, bạn sẽ cần một máy lớn hơn và đắt hơn nhiều, và ngay cả khi đó, 4K vẫn là một lựa chọn mạo hiểm.
Hãy tính toán cả băng thông, vì đó là một bất ngờ khác. Direct play gửi file với bitrate của chính nó. Một file 1080p đã nén chạy ở mức 8-12 Mbps; một bản 1080p Blu-ray remux chạy 20-30 Mbps; 4K HDR chạy 40-80 Mbps. Ba người cùng direct-play các file 10 Mbps sẽ tiêu tốn 30 Mbps upload liên tục từ VPS của bạn. Hãy kiểm tra hai thông số trong gói cước của bạn: tốc độ port (có thể đẩy 30 Mbps upstream không?) và giới hạn transfer hàng tháng. Một bộ phim 10 Mbps dài hai tiếng tiêu tốn khoảng 9 GB, vì vậy một hạn mức 1 TB/tháng chỉ đủ cho hơn một trăm bộ phim như vậy mỗi tháng — khoảng ba hoặc bốn phim mỗi ngày — và một hộ gia đình xem 4K, với bitrate gấp bốn đến tám lần, sẽ tiêu tốn dung lượng nhanh hơn nhiều.
Điều kiện tiên quyết
- Một VPS Ubuntu 24.04 KVM mới có quyền root hoặc sudo, đã cài đặt Docker và plugin Compose.
- Một volume block-storage cho media, dung lượng phù hợp với thư viện của bạn (xem phần tính toán bên dưới). Ổ root nhỏ đi kèm VPS không phải là nơi để chứa phim.
- Một tên miền nếu bạn muốn truy cập HTTPS công khai, hoặc một WireGuard VPN trên cùng VPS đó nếu bạn muốn giữ mọi thứ riêng tư.
- Media mà bạn có quyền phát hợp pháp — các bản rip của chính bạn, các bản ghi của chính bạn, các file bạn sở hữu.
Mount block storage trước tiên
Attach volume trong panel của nhà cung cấp, sau đó tìm và mount nó. Lấy tên thiết bị từ lsblk — nó sẽ có dạng /dev/sdb hoặc /dev/vdb, không bao giờ là ổ root.
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 deviceMount nó bằng UUID, không phải bằng /dev/sdb, vì các ký tự thiết bị có thể thay đổi thứ tự sau khi reboot và bạn có thể vô tình format hoặc mount nhầm ổ đĩa. Thêm một dòng vào /etc/fstab:
UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /mnt/media ext4 defaults,nofail 0 2sudo mount -a
df -h /mnt/medianofail rất quan trọng: nếu không có nó, nếu block volume bị detach, máy sẽ không thể boot và rơi vào emergency shell. Sai lầm lớn nhất ở đây là chạy mkfs.ext4 trên một volume đã có dữ liệu — nó sẽ xóa sạch dữ liệu đó. Chỉ format các volume mới; nếu ổ đĩa đã có thư viện của bạn, hãy bỏ qua và đi thẳng đến dòng fstab.
Sắp xếp media theo cách Jellyfin yêu cầu
Jellyfin khớp metadata dựa trên tên folder và tên file. Nếu sắp xếp sai, phim sẽ hiển thị là các file không tên và không có poster, hoặc một tập phim bị khớp nhầm với series khác. Chỉ có đúng ba quy tắc: mỗi bộ phim nằm trong một folder Name (Year) riêng biệt với tên file tương ứng; các folder season được đặt tên là Season 01, không phải S01; các file tập phim sử dụng S01E01; và các phần đặc biệt (specials) nằm trong 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.mkvPhần (Year) trên phim không phải để trang trí — nó giúp phân biệt các bản remake để trình khớp metadata lấy đúng tiêu đề. Hãy giữ Movies và Shows thành các folder cấp cao nhất riêng biệt vì mỗi cái sẽ trở thành một Jellyfin library cho một loại nội dung cụ thể, và việc trộn lẫn chúng sẽ làm rối trình cung cấp metadata.
Permissions: lý do số một khiến thư viện trống rỗng
Đây là sự hiểu lầm gây lãng phí thời gian của nhiều người. Image jellyfin/jellyfin chính thức không hỗ trợ các biến môi trường PUID/PGID — những biến đó thuộc về image của LinuxServer.io (lscr.io/linuxserver/jellyfin). Với image chính thức, bạn kiểm soát user bằng key user: trong compose, và nếu bạn bỏ qua nó, container sẽ chạy dưới quyền root. Dù bạn dùng cách nào, quy tắc vẫn giống nhau: uid/gid mà container chạy phải có quyền đọc và truy cập (traverse) mọi thư mục media.
Chúng ta sẽ chạy với uid/gid 1000, user không phải root đầu tiên trên một máy Ubuntu tiêu chuẩn. Hãy xác nhận uid của bạn và thiết lập ownership:
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 ~/jellyfinCác thư mục cần có quyền execute (quyền x trong 755), không chỉ quyền read — nếu không có nó, container không thể truy cập vào thư mục mặc dù nó có thể liệt kê tên file. Cái bẫy khiến cả thư viện bị trống chính là folder cha: nếu uid của container không thể truy cập vào chính mount point, nó sẽ không bao giờ chạm tới được /media/Movies hay /media/Shows, và mọi thư viện sẽ trống rỗng cùng một lúc với lỗi Access to the path ... is denied trong log. Bất kỳ folder media đơn lẻ nào mà nó không thể đọc sẽ bị log lại và bị bỏ qua, vì vậy một loạt file được copy vào bằng quyền root sẽ biến mất lặng lẽ khỏi thư viện. Đây là lý do tại sao chúng ta dùng chown đệ quy và thiết lập quyền execute cho mọi thư mục thay vì chỉ sửa một folder.
File 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.comGiải thích từng dòng: user: "1000:1000" là thứ thực sự thiết lập file permissions, khớp với ownership ở trên. /config chứa toàn bộ server — tài khoản, thư viện, metadata, trạng thái xem — vì vậy nó phải có quyền ghi và là thứ bạn cần backup. /cache là không gian làm việc tạm thời. Mount media là :ro (chỉ đọc) một cách có chủ đích: Jellyfin mặc định lưu artwork và metadata trong /config, nên nó không bao giờ cần ghi vào thư viện của bạn, và chế độ read-only giúp bảo vệ file khỏi việc vô tình bị xóa hoặc do một plugin lỗi. Port được bind vào 127.0.0.1 một cách có chủ đích — web login của Jellyfin là HTTP thuần, nên chúng ta không bao giờ publish port 8096 ra internet công cộng. JELLYFIN_PublishedServerUrl là địa chỉ mà server dùng để quảng bá cho việc tự động phát hiện cục bộ (local autodiscovery) — một bản tin UDP broadcast trong LAN, nên các client từ internet sẽ không thấy nó và chỉ đơn giản sử dụng URL mà bạn nhập vào ứng dụng. Hãy đặt nó là địa chỉ mà các client nên được thông báo, và bạn sẽ phải nhập URL đó bằng tay trên các thiết bị từ xa.
Chạy container từ thư mục compose:
docker compose up -d
docker logs -f jellyfinChạy lần đầu: trình thiết lập và các thư viện của bạn
Vì port được bind vào localhost, hãy truy cập trình thiết lập thông qua một SSH tunnel từ laptop của bạn thay vì mở một lỗ trên firewall:
ssh -L 8096:127.0.0.1:8096 you@your-vps-ipBây giờ hãy truy cập vào http://localhost:8096. Trình thiết lập sẽ hướng dẫn bạn chọn ngôn ngữ, sau đó tạo một admin user với mật khẩu mạnh — tài khoản này là server của bạn, vì vậy đừng dùng mật khẩu tạm bợ. Thêm thư viện đầu tiên: chọn content type là Movies, trỏ nó vào /media/Movies (đường dẫn bên trong container, không phải đường dẫn trên host), và lặp lại với Shows tại /media/Shows. Hoàn tất, và Jellyfin sẽ scan. Kết quả đúng là poster và tiêu đề sẽ xuất hiện trong vòng một đến hai phút với một thư viện nhỏ. Thêm hoặc chỉnh sửa thư viện sau đó trong Dashboard → Libraries, và ép buộc quét lại bằng Scan All Libraries.
Nếu bạn có sử dụng bất kỳ hình thức transcoding nào, hãy mở Dashboard → Playback → Transcoding và đặt transcode temp path là /cache/transcodes để các file tạm nằm ở volume cache thay vì làm đầy /config. Để hardware acceleration ở chế độ None — vì không có GPU để tăng tốc.
Truy cập từ xa: TLS reverse proxy, hoặc giữ nó trong VPN
Bạn có hai cách an toàn để truy cập Jellyfin từ bên ngoài, và một cách không an toàn cần tránh. Cách không an toàn là publish port 8096 trực tiếp ra internet: thông tin đăng nhập sẽ truyền dưới dạng cleartext và port này sẽ bị brute-force trong vòng vài giờ.
Option A — TLS reverse proxy. Đặt Jellyfin sau một subdomain sử dụng Traefik với automatic TLS cho các Docker apps của bạn, hoặc sau nginx với một chứng chỉ Let's Encrypt được cấp bởi Certbot. Jellyfin sử dụng WebSockets để cập nhật thời gian thực, vì vậy proxy phải chuyển tiếp các upgrade headers. Traefik làm việc này tự động; nginx cần phải khai báo rõ ràng, và cần HTTP/1.1 tới upstream nếu không việc upgrade sẽ không xảy ra:
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";
}Đặt JELLYFIN_PublishedServerUrl thành địa chỉ https:// để bất kỳ tính năng tự động phát hiện cục bộ nào cũng quảng bá đúng URL — các ứng dụng từ xa sẽ sử dụng địa chỉ bạn cung cấp — và thêm fail2ban để làm chậm các nỗ lực brute-force đối với login. Khi server đã public, hãy trỏ Uptime Kuma vào URL đó để bạn biết về tình trạng downtime trước khi người xem của bạn biết.
Option B — giữ nó riêng tư trong VPN. Không publish port 8096. Chỉ truy cập Jellyfin thông qua một tunnel WireGuard kết thúc tại chính máy đó. Đối với một hộ gia đình, đây là lựa chọn bảo mật đơn giản nhất — không cần chứng chỉ, không lộ diện công khai, không có bề mặt tấn công brute-force. Bind container vào địa chỉ tunnel hoặc localhost và kết nối qua VPN. Xem thiết lập WireGuard VPN cho một VPS riêng tư để biết cách dựng tunnel.
Kích thước lưu trữ và backup
Hãy tính toán theo chất lượng, không phải số lượng file. Phim 1080p nén chạy khoảng 4-15 GB mỗi phim; một bản 1080p remux chạy 20-40 GB; một season TV 1080p chạy 15-40 GB; bất kỳ thứ gì 4K chạy 40-100 GB mỗi phim. Một thư viện gồm vài trăm phim cộng với một số show cần một volume 2-4 TB, và việc cấp dư (over-provision) block volume một lần sẽ rẻ hơn là phải migrate sau này.
/config là toàn bộ trạng thái server, vì vậy đó là thứ duy nhất bạn bắt buộc phải backup. Hãy snapshot hoặc stop-and-tar nó và giữ bản copy ở ngoài máy:
docker compose down
sudo tar czf jellyfin-config-$(date +%F).tgz -C ~/jellyfin config
docker compose up -d/cache và thư mục transcode là các thư mục có thể bỏ đi. Media trên /mnt/media bạn hãy backup riêng hoặc chấp nhận nếu phải rip lại — hầu hết mọi người chọn cách sau do dung lượng quá lớn. Các bản cập nhật là docker compose pull && docker compose up -d; tag :10 ở trên sẽ nằm trong phiên bản 10.x, vì vậy việc chuyển sang phiên bản major tiếp theo là một sự thay đổi tag có chủ đích — hãy đọc qua release notes của Jellyfin trước khi thực hiện, vì việc migration schema thư viện thường xảy ra ở các phiên bản major.
Các lỗi thường gặp, kèm theo các thông báo bạn sẽ thấy
Thư viện trống sau khi scan. Log tại Dashboard → Logs (hoặc ~/jellyfin/config/log/log_*.log) hiển thị:
System.UnauthorizedAccessException: Access to the path '/media/Movies' is denied.uid của container không thể đọc đường dẫn đó. Nguyên nhân: media thuộc sở hữu của root hoặc một uid khác với giá trị user: của bạn, một thư mục thiếu quyền execute, hoặc chính mount point cha không thể truy cập bởi uid đó. Cách sửa: chown -R 1000:1000 /mnt/media, các thư mục 755, các file 644, sau đó rescan.
Playback làm treo CPU và bị buffer. docker stats jellyfin cho thấy CPU gần mức 100% nhân với số lượng core, và Dashboard → Playback liệt kê session là Transcode với tốc độ dưới 1.0x. Client không thực hiện direct-play, nên VPS đang transcode bằng CPU chậm hơn thời gian thực và bị mất frame. Nguyên nhân: codec hoặc container không được hỗ trợ, burn-in phụ đề, hoặc HDR tone-mapping. Cách sửa: chuyển sang client hỗ trợ direct-play, giữ source ở định dạng H.264/AAC, sử dụng phụ đề dạng text (SRT) thay vì phụ đề dạng image (PGS/VOBSUB) vốn bắt buộc phải burn-in, và đừng chạy 4K HDR trên một máy chỉ có CPU.
"No compatible streams are available." Thông báo đầy đủ thường là "This client isn't compatible with the media and the server isn't sending a compatible media format." Client đã từ chối source và quá trình transcode dự phòng cũng không thể bắt đầu. Nguyên nhân: lệnh ffmpeg bị lỗi, file không đọc được, hoặc profile của user đang chặn chuyển đổi video. Cách sửa: đọc dòng ffmpeg trong Dashboard → Logs, xác nhận file có chơi được không, kiểm tra quyền playback của user nếu bạn dùng transcoding, và thử một client khác để loại trừ lỗi codec của trình duyệt.
Phim không có poster hoặc sai poster. Metadata không khớp. Nguyên nhân: một bộ phim không nằm trong folder Name (Year) riêng biệt, một folder season đặt tên là S01 thay vì Season 01, các tập phim không ở dạng S01E01, hoặc thiếu năm sản xuất. Cách sửa: đổi tên theo layout ở trên, sau đó chọn Refresh metadata → Replace all, hoặc dùng Identify trên một item để khớp đúng entry TMDB/TVDB.
FAQ
Một VPS có thể transcode video mà không có GPU không?
Có, nhưng chỉ bằng CPU, và nó rất tốn kém. Một lượt transcode phần mềm 1080p duy nhất có thể làm quá tải vài vCPU, và 4K hoặc HEVC thường không thể theo kịp thời gian thực, dẫn đến buffer. Cách tốt nhất là tránh transcoding: giữ thư viện ở H.264/AAC và dùng các ứng dụng client hỗ trợ direct-play, để VPS chỉ việc stream các byte. Chỉ thuê instance có GPU nếu bạn thực sự cần transcode ngay lập tức.
Tại sao thư viện Jellyfin của tôi trống rỗng sau khi scan?
Hầu hết luôn là do permission. Image jellyfin/jellyfin chính thức chạy với bất kỳ user: nào bạn thiết lập (hoặc root), và nếu các file không thể được đọc bởi uid đó, quá trình scan sẽ log Access to the path ... is denied và bỏ qua chúng. Hãy sửa ownership bằng chown -R 1000:1000 /mnt/media, cấp quyền execute cho các thư mục (755), và rescan — đồng thời kiểm tra cả folder cha, vì nếu uid của container không thể truy cập vào /mnt/media, nó sẽ không bao giờ chạm tới được các folder thư viện và mọi thứ sẽ trống rỗng. Nguyên nhân phổ biến thứ hai là cấu trúc folder không khớp với những gì Jellyfin yêu cầu.
Làm thế nào để truy cập Jellyfin từ xa một cách an toàn?
Có hai lựa chọn tốt. Đặt nó sau một TLS reverse proxy trên một subdomain để việc đăng nhập và stream được mã hóa, và thêm fail2ban — đừng bao giờ để lộ port 8096 thuần, vì nó gửi mật khẩu dưới dạng cleartext. Hoặc giữ nó hoàn toàn riêng tư và chỉ truy cập qua VPN, đây là lựa chọn an toàn đơn giản nhất cho một hộ gia đình. Hãy cung cấp cho các ứng dụng địa chỉ public trực tiếp — autodiscovery là một bản tin broadcast trong mạng nội bộ, nên nó không đến được các client từ internet.
Một Jellyfin VPS cần bao nhiêu đĩa và băng thông?
Dung lượng đĩa phụ thuộc vào chất lượng: dự tính 4-15 GB cho mỗi phim 1080p nén, 20-40 GB cho một bản remux, và 40-100 GB cho 4K, vì vậy hầu hết các thư viện cần một block volume 2-4 TB. Băng thông được quyết định bởi bitrate của direct-play — 8-12 Mbps cho mỗi stream 1080p, nhiều hơn nhiều cho 4K — vì vậy hãy xác nhận tốc độ port có xử lý được số lượng người xem đồng thời hay không và chú ý hạn mức transfer hàng tháng. Hãy chuẩn bị dư CPU nếu bạn có kế hoạch transcode; ưu tiên băng thông hơn số nhân nếu bạn định dùng direct-play.
Chạy Jellyfin trên VPS có hợp pháp không?
Bản thân Jellyfin là phần mềm miễn phí, open-source và việc chạy nó là hoàn toàn hợp pháp. Điều quan trọng là nội dung: chỉ stream những media mà bạn sở hữu hoặc có bản quyền — các bản rip đĩa của chính bạn, các bản ghi, hoặc các file bạn có quyền sử dụng. Jellyfin không cung cấp sẵn media và không có cách nào để lấy media; nó là một trình phát cho một thư viện mà bạn đã sở hữu sẵn.