Cài Jellyfin trên VPS bằng Docker để stream media
Dựng Jellyfin trên VPS bằng Docker với compose 15 dòng, block storage và quyền file đúng. Biết khi nào direct play đủ dùng, khi nào CPU transcoding quá tải.
Bạn sẽ xây dựng gì
Một media server Jellyfin trên VPS: một container, ba volume và một disk block storage chứa phim và chương trình của bạn, có thể truy cập từ mọi browser hoặc Jellyfin app. Việc cài đặt chỉ cần một file compose gồm 15 dòng. Mọi lỗi phát sinh sau đó đều đến từ 2 nguyên nhân: container không thể đọc file do quyền truy cập, và bạn yêu cầu một VPS không có GPU transcode video dù phần cứng này không phù hợp cho việc đó. Phần lớn nội dung của guide tập trung vào 2 vấn đề này, vì đây là nguyên nhân của hầu hết support ticket.
Jellyfin miễn phí và hoàn toàn open source, không yêu cầu account, không có tính năng thu phí và không thu thập telemetry. Vì vậy, Jellyfin gần như luôn xuất hiện trong các danh sách những thứ đáng tự host trong năm 2026. Jellyfin phát media mà bạn sở hữu. Jellyfin không cung cấp content, và guide này không hướng dẫn cách có được content đó.
Thực tế về transcoding trước khi bạn thuê bất kỳ thứ gì
Đọc phần này trước, vì nó quyết định loại máy bạn cần thuê. Media server sẽ làm một trong hai việc khi bạn nhấn play. Direct play stream nguyên file: VPS đọc dữ liệu từ disk rồi đẩy qua network, gần như không tốn CPU. Transcoding mã hóa lại video trong lúc phát, chẳng hạn đổi độ phân giải, codec hoặc burn phụ đề vào hình ảnh. Công việc này hoàn toàn do CPU xử lý.
VPS thông thường không có GPU. Vì vậy, mọi tác vụ transcoding đều chạy trên CPU bằng libx264/libx265, và software encoding rất tốn tài nguyên. Một tác vụ transcoding H.264 1080p có thể làm bão hòa nhiều vCPU dùng chung; transcoding 4K hoặc HEVC thường hoàn toàn không theo kịp thời gian thực, khiến playback bị dừng và buffer liên tục. Hardware transcoding, yếu tố giúp việc này rẻ trên máy tại nhà có Intel iGPU hoặc card Nvidia, đơn giản là không có sẵn cho bạn trừ khi provider cho thuê GPU instance.
Vì vậy, chiến lược trên VPS là: tránh transcoding. Lưu thư viện bằng các codec mà client có thể phát native, gồm video H.264, audio AAC hoặc AC3, trong container MP4 hoặc MKV. Chọn các client app hỗ trợ direct play: app Jellyfin native cho Android TV, iOS và Roku, cùng với Infuse, Kodi và Jellyfin Media Player trên desktop. Khi đó VPS không phải chạy ffmpeg, và một máy 2 vCPU vừa phải có thể stream cho nhiều người cùng lúc. Nếu dự định transcoding, bạn cần một máy lớn hơn và đắt hơn nhiều. Ngay cả khi đó, 4K vẫn là lựa chọn rủi ro.
Hãy tính cả băng thông, vì đây cũng là một điểm thường gây bất ngờ. Direct play gửi file ở đúng bitrate của file. File 1080p đã nén thường dùng 8-12 Mbps; bản remux Blu-ray 1080p dùng 20-30 Mbps; 4K HDR dùng 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. Kiểm tra 2 thông số trong gói dịch vụ: tốc độ port (có thể đẩy 30 Mbps upstream không?) và giới hạn transfer hằng tháng. Một phim 10 Mbps dài 2 giờ tạo ra khoảng 9 GB traffic outbound, nên gói metered giới hạn 1 TB/tháng chỉ đáp ứng được hơn một trăm phim như vậy mỗi tháng, tương đương 3 hoặc 4 phim mỗi ngày. Một hộ gia đình xem 4K, với bitrate cao gấp 4 đến 8 lần, sẽ làm cạn giới hạn nhanh hơn nhiều. Hãy tính mọi traffic outbound khác từ cùng máy đó vào cùng ngân sách, bao gồm cả một RustDesk relay tự host, vì relay này mang toàn bộ phiên remote desktop bất cứ khi nào 2 peer không thể kết nối trực tiếp.
Điều kiện cần
- Một KVM VPS Ubuntu 24.04 mới với quyền root hoặc sudo, đã cài Docker và Compose plugin.
- Một volume block storage dành cho media, có dung lượng phù hợp với thư viện của bạn (xem phần ước tính dung lượng bên dưới). Đĩa root nhỏ đi kèm VPS không phải nơi để lưu phim.
- Một domain nếu bạn muốn public HTTPS, hoặc WireGuard VPN trên cùng VPS nếu bạn muốn giữ toàn bộ hệ thống ở chế độ private.
- Media mà bạn có quyền stream hợp pháp, gồm các bản rip của chính bạn, bản ghi của chính bạn và các file thuộc quyền sở hữu của bạn.
Gắn block storage trước
Gắn volume trong panel của nhà cung cấp, sau đó xác định và mount volume đó. Lấy tên device từ lsblk. Tên này sẽ có dạng như /dev/sdb hoặc /dev/vdb, không bao giờ là 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 deviceMount bằng UUID, không dùng /dev/sdb, vì tên device có thể thay đổi thứ tự sau mỗi lần reboot và bạn có thể format hoặc mount nhầm disk. 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 block volume bị detach, máy sẽ không boot được và chuyển vào emergency shell. Sai lầm nghiêm trọng nhất ở đây là chạy mkfs.ext4 trên volume đã chứa dữ liệu vì lệnh này sẽ xóa toàn bộ dữ liệu. Chỉ format volume mới. Nếu disk đã chứa thư viện dữ liệu của bạn, bỏ qua bước này và chuyển thẳng đến dòng fstab.
Bố trí media theo đúng cấu trúc Jellyfin
Jellyfin đối chiếu metadata dựa trên tên thư mục và tên file. Nếu bố trí sai, phim sẽ xuất hiện dưới dạng file không có tên và poster, hoặc tập phim sẽ bị ghép nhầm series. Có đúng ba quy tắc: mỗi phim nằm trong một thư mục Name (Year) riêng và có tên file khớp với tên thư mục; thư mục mùa phải có tên Season 01, không phải S01; file tập phim dùng S01E01; còn các tập đặc biệt đặt 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.mkv(Year) trong tên phim không phải để trang trí; nó giúp phân biệt các bản làm lại để bộ đối chiếu chọn đúng tên phim. Giữ Movies và Shows thành các thư mục cấp cao nhất riêng biệt vì mỗi thư mục sẽ trở thành một Jellyfin library cho một loại nội dung cụ thể. Trộn chúng sẽ làm nhà cung cấp metadata đối chiếu sai. Jellyfin vẫn có thể lập chỉ mục thư mục ảnh thứ ba, nhưng trải nghiệm sẽ hạn chế hơn so với photo server chuyên dụng. Vì vậy, nếu album ảnh quan trọng với bạn, hãy đặt chúng trên một máy riêng chạy PhotoPrism hoặc Immich và để máy này chỉ phục vụ phim và TV.
Quyền: nguyên nhân hàng đầu khiến thư viện không có dữ liệu
Đây là hiểu lầm khiến nhiều người mất cả buổi tối. Image jellyfin/jellyfin chính thức không dùng các biến môi trường PUID/PGID; các biến này thuộc image của LinuxServer.io (lscr.io/linuxserver/jellyfin). Với image chính thức, bạn chỉ định user bằng khóa user: trong compose. Nếu bỏ qua khóa này, container sẽ chạy dưới quyền root. Dù dùng cách nào, nguyên tắc vẫn giống nhau: uid/gid mà container chạy dưới đó phải có quyền đọc và traverse mọi thư mục media.
Chúng ta sẽ chạy dưới uid/gid 1000, là user không phải root đầu tiên trên một máy Ubuntu mặc định. Hãy xác nhận uid/gid của bạn rồi đặt owner:
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 ~/jellyfinThư mục cần có bit execute (bit x trong 755), không chỉ quyền đọc. Nếu thiếu bit này, container không thể vào thư mục dù vẫn có thể liệt kê tên thư mục. Lỗi khiến cả thư viện không có dữ liệu thường nằm ở thư mục cha: nếu uid của container không thể traverse chính mount đó, nó sẽ không bao giờ tới được /media/Movies hoặc /media/Shows. Khi đó, mọi thư viện sẽ cùng lúc không có dữ liệu và log sẽ ghi Access to the path ... is denied. Chỉ cần một thư mục media bất kỳ không thể đọc là thư mục đó sẽ được ghi log và bỏ qua. Vì vậy, một loạt file được copy vào dưới quyền root có thể âm thầm biến mất khỏi thư viện. Đây là lý do chúng ta chown đệ quy và đặt bit execute trên mọi thư mục, thay vì chỉ sửa một thư mục.
Tệp 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à phần thực sự thiết lập quyền file, khớp với ownership ở trên. /config chứa toàn bộ server, account, library, metadata và trạng thái theo dõi, nên phải cho phép ghi và là phần bạn cần backup. /cache là vùng làm việc có thể xóa. Mount media là :ro (chỉ đọc) có chủ ý: Jellyfin mặc định lưu artwork và metadata trong /config, nên không bao giờ cần ghi vào library của bạn; chế độ chỉ đọc cũng bảo vệ file khỏi thao tác xóa ngoài ý muốn hoặc plugin lỗi. Port được bind vào 127.0.0.1 có chủ ý. Trang đăng nhập web của Jellyfin dùng HTTP thuần, vì vậy chúng ta không public port 8096 ra Internet. JELLYFIN_PublishedServerUrl là địa chỉ server quảng bá để các client tự discovery trong mạng nội bộ, thông qua LAN UDP broadcast. Do đó, các client trên Internet không bao giờ thấy địa chỉ này và chỉ sử dụng URL bạn nhập vào app. Hãy đặt giá trị này thành địa chỉ mà client cần nhận, đồng thời chuẩn bị nhập URL đó thủ công trên các thiết bị remote.
Khởi động từ thư mục compose:
docker compose up -d
docker logs -f jellyfinLần chạy đầu tiên: trình hướng dẫn thiết lập và các thư viện
Vì cổng chỉ bind vào localhost, hãy truy cập trình hướng dẫn qua SSH tunnel từ laptop thay vì mở thêm rule trên firewall:
ssh -L 8096:127.0.0.1:8096 you@your-vps-ipBây giờ mở http://localhost:8096 trong trình duyệt. Trình hướng dẫn sẽ yêu cầu chọn ngôn ngữ, sau đó tạo admin user với mật khẩu mạnh. Tài khoản này dùng để quản trị server, vì vậy không được dùng lại mật khẩu tạm thời. Thêm library đầu tiên: chọn loại nội dung Movies, trỏ đến /media/Movies (đường dẫn bên trong container, không phải đường dẫn trên host), rồi lặp lại với Shows tại /media/Shows. Hoàn tất để Jellyfin bắt đầu scan. Nếu thiết lập đúng, poster và tiêu đề của một library nhỏ sẽ xuất hiện trong vòng 1–2 phút. Sau này, bạn có thể thêm hoặc chỉnh sửa library tại Dashboard → Libraries, rồi buộc scan lại bằng Scan All Libraries. Các poster này cũng là lý do nên xem thử frontend thay thế sau khi những chức năng cơ bản đã hoạt động, vì Halcyon dựng lại cùng library thành một cửa hàng video kiểu thập niên 90 có thể đi dạo từ artwork mà Jellyfin đã tải xuống.
Nếu bạn có dùng transcoding, hãy mở Dashboard → Playback → Transcoding và đặt đường dẫn tạm cho transcode thành /cache/transcodes để dữ liệu phát sinh được ghi vào cache volume thay vì làm đầy /config. Giữ tăng tốc phần cứng ở None, vì không có GPU để thực hiện việc tăng tốc.
Truy cập từ xa: reverse proxy TLS hoặc giữ trong VPN
Bạn có 2 cách an toàn để truy cập Jellyfin từ bên ngoài và 1 cách không an toàn cần tránh. Cách không an toàn là đưa thẳng port 8096 ra Internet: thông tin đăng nhập truyền dưới dạng plaintext và port này sẽ bị brute-force chỉ sau vài giờ.
Tùy chọn A, reverse proxy TLS. Đặt Jellyfin trên một subdomain phía sau Traefik với TLS tự động cho các ứng dụng Docker hoặc phía sau nginx với certificate Let's Encrypt được cấp bằng Certbot. Jellyfin dùng WebSockets để cập nhật theo thời gian thực, nên proxy phải chuyển tiếp các header upgrade. Traefik tự làm việc này; nginx cần khai báo rõ các header đó và phải dùng HTTP/1.1 đến upstream, nếu không quá trình 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:// để tính năng autodiscovery nội bộ quảng bá đúng URL, các app từ xa sử dụng địa chỉ bạn cung cấp, rồi thêm fail2ban để làm chậm các lần brute-force nhằm vào đăng nhập. Khi server đã public, trỏ Uptime Kuma đến URL để bạn biết server downtime trước khi người xem phát hiện. Trỏ notification của nó đến server ntfy tự host để một lần kiểm tra thất bại gửi push đến điện thoại thay vì email mà bạn phải đến sáng hôm sau mới đọc. Việc đặt trang đăng nhập trên Internet public cũng là thời điểm phù hợp để audit phần còn lại của máy, thay vì cho rằng chỉ port 443 đang trả lời, và open-kritt chạy quá trình scan từ container riêng trên cùng VPS.
Tùy chọn B, giữ private trong VPN. Không public port 8096; chỉ truy cập Jellyfin qua tunnel WireGuard kết thúc trên chính máy đó. Với mạng gia đình, đây là lựa chọn bảo mật đơn giản nhất: không cần certificate, không public dịch vụ và không có bề mặt brute-force. Bind container vào địa chỉ của tunnel hoặc localhost rồi kết nối qua VPN. Xem hướng dẫn thiết lập WireGuard VPN cho VPS private để cấu hình tunnel.
Kích thước lưu trữ và backup
Hãy lập ngân sách theo chất lượng, không theo số lượng file. Phim 1080p đã nén thường chiếm 4-15 GB mỗi phim; bản remux 1080p chiếm 20-40 GB; một mùa phim truyền hình 1080p chiếm 15-40 GB; nội dung 4K thường chiếm 40-100 GB mỗi phim. Một thư viện vài trăm phim cùng một số chương trình truyền hình cần volume 2-4 TB. Mở rộng dư block volume ngay từ đầu thường rẻ hơn việc migrate sau này.
/config là toàn bộ trạng thái của server, vì vậy đây là thứ duy nhất bạn bắt buộc phải backup. Hãy tạo snapshot hoặc stop container rồi tar thư mục này, sau đó lưu bản sao bên ngoài server:
docker compose down
sudo tar czf jellyfin-config-$(date +%F).tgz -C ~/jellyfin config
docker compose up -d/cache và thư mục transcode có thể xóa và tạo lại. Media trên /mnt/media nên được backup riêng hoặc chấp nhận việc phải rip lại. Do dung lượng lớn, hầu hết mọi người chọn cách thứ hai. Các bản nâng cấp dùng docker compose pull && docker compose up -d; tag :10 ở trên vẫn nằm trong major 10.x. Vì vậy, chuyển sang major tiếp theo là một thay đổi tag có chủ đích. Hãy đọc lướt release notes của Jellyfin trước khi thực hiện, vì migration schema của thư viện xảy ra ở các major version. Một tag được pin cùng một thư mục state đã backup là toàn bộ công thức cho mọi container chạy liên tục. Đây cũng là pattern giúp giữ memory và lịch chạy của agent tự host hoạt động sau các lần reboot.
Các lỗi thường gặp và chuỗi log tương ứng
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 có thể là media thuộc về root hoặc một uid khác với giá trị user:, thư mục thiếu quyền execute, hoặc mount parent không cho uid đó duyệt qua. Cách khắc phục: chown -R 1000:1000 /mnt/media, đặt quyền cho thư mục là 755, cho file là 644, rồi scan lại.
Playback làm CPU chạy hết công suất và bị buffering. docker stats jellyfin hiển thị CPU gần 100% nhân với số core, còn Dashboard → Playback liệt kê session ở trạng thái Transcode với tốc độ thấp hơn 1.0x. Client không direct-play, nên VPS phải transcode bằng CPU chậm hơn thời gian thực và không theo kịp playback. Nguyên nhân có thể là codec hoặc container không được hỗ trợ, phải burn-in phụ đề, hoặc phải tone-mapping HDR. Cách khắc phục: chuyển sang client hỗ trợ direct-play, giữ nguồn ở H.264/AAC, dùng phụ đề dạng text (SRT) thay vì phụ đề dạng image (PGS/VOBSUB), vì loại này buộc phải burn-in, và không chạy nội dung 4K HDR trên máy chỉ có CPU.
"Không có stream tương thích." Thông báo đầy đủ thường là "Client này không tương thích với media và server không gửi được định dạng media tương thích." Client đã từ chối nguồn, còn transcode dự phòng cũng không khởi động được. Nguyên nhân có thể là lệnh ffmpeg bị lỗi, file không đọc được, hoặc profile của user chặn chuyển đổi video. Cách khắc phục: đọc dòng ffmpeg trong Dashboard → Logs, xác nhận file có thể playback, kiểm tra quyền playback của user nếu bạn phụ thuộc vào transcoding, và thử client thứ hai để loại trừ lỗi codec riêng của browser.
Film không có poster hoặc poster bị sai. Metadata không khớp. Nguyên nhân có thể là film không nằm trong thư mục Name (Year) riêng, thư mục season được đặt tên là S01 thay vì Season 01, episode không theo dạng S01E01, hoặc thiếu năm. Cách khắc phục: đổi tên theo layout ở trên, rồi chọn Refresh metadata → Replace all, hoặc dùng Identify cho một item để cố định entry TMDB/TVDB chính xác.
FAQ
VPS có thể transcode video mà không cần GPU không?
Có, nhưng chỉ dùng CPU và chi phí rất cao. Một lần software transcode 1080p có thể làm bão hòa nhiều vCPU, còn 4K hoặc HEVC thường không theo kịp thời gian thực nên phát video bị buffering. Cách hiệu quả là tránh transcoding: lưu thư viện ở định dạng H.264/AAC và dùng client app hỗ trợ direct-play, để VPS chỉ truyền dữ liệu. Chỉ thuê GPU instance khi bạn thực sự cần transcoding theo thời gian thực.
Tại sao thư viện Jellyfin trống sau khi scan?
Gần như luôn là do quyền truy cập. Image jellyfin/jellyfin chính thức chạy bằng user: mà bạn đã đặt (hoặc root). Nếu uid đó không có quyền đọc các file, log scan sẽ ghi Access to the path ... is denied rồi bỏ qua chúng. Sửa owner bằng chown -R 1000:1000 /mnt/media, cấp quyền execute cho các thư mục bằng 755 rồi scan lại. Đồng thời kiểm tra cả thư mục cha, vì nếu uid của container không thể traverse chính /mnt/media thì nó không bao giờ đến được các thư mục thư viện và kết quả luôn trống. Nguyên nhân phổ biến thứ hai là layout thư mục không đúng với yêu cầu của Jellyfin.
Làm thế nào để truy cập Jellyfin từ xa an toàn?
Có 2 lựa chọn tốt. Đặt Jellyfin sau một TLS reverse proxy trên subdomain để mã hóa thông tin đăng nhập và stream, đồng thời thêm fail2ban. Không bao giờ expose port 8096 dạng plain, vì cách này gửi password dưới dạng cleartext. Hoặc giữ Jellyfin hoàn toàn private và chỉ truy cập qua VPN; đây là lựa chọn an toàn và đơn giản nhất cho gia đình. Cấu hình public address trực tiếp cho các app. Autodiscovery là broadcast trong local network nên không đến được các client truy cập từ Internet.
VPS chạy Jellyfin cần bao nhiêu disk và bandwidth?
Dung lượng disk phụ thuộc vào chất lượng: dự trù 4-15 GB cho mỗi phim 1080p đã nén, 20-40 GB cho mỗi remux và 40-100 GB cho 4K. Vì vậy, hầu hết thư viện cần một block volume 2-4 TB. Bandwidth phụ thuộc vào bitrate khi direct-play: 8-12 Mbps cho mỗi stream 1080p và cao hơn nhiều đối với 4K. Hãy xác nhận port speed đủ cho số người xem đồng thời và theo dõi monthly transfer cap. Nếu dự định transcode, hãy chừa thêm CPU headroom. Nếu dự định direct-play, hãy ưu tiên bandwidth hơn số core.
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í, mã nguồn mở và việc chạy phần mềm này hoàn toàn hợp pháp. Điều quan trọng là nội dung: chỉ stream media mà bạn sở hữu hoặc được cấp phép lưu trữ, chẳng hạn bản rip từ đĩa của bạn, bản ghi hoặc các file mà bạn có quyền sử dụng. Jellyfin không kèm media và không cung cấp cách lấy media. Đây là trình phát cho thư viện mà bạn đã sở hữu.