VPS Tutorials
Deploying Jellyfin on a VPS (2026): Docker, Hardware Transcoding, HTTPS, Media Libraries and Remote Playback
Deploy Jellyfin on a VPS with Docker Compose, configure media libraries, domain HTTPS, subtitles, remote playback and Intel/NVIDIA hardware transcoding, and get clear guidance on bandwidth costs, storage, backup and upgrades, and common failures.

Deploying Jellyfin on a VPS (2026): Docker, Hardware Transcoding, HTTPS, Media Libraries and Remote Playback
If you want to organize your own movies, TV shows, music and home videos into a Netflix-like interface without handing your media library to a third-party platform, Jellyfin is one of the most common open-source choices. It offers web, TV, mobile and desktop clients, and supports media scraping, subtitles, multiple users, resume playback, hardware transcoding and remote playback.
However, before deploying Jellyfin on a VPS you must first work out three things: where the media will live, whether playback requires transcoding, and whether outbound traffic is sufficient. Jellyfin itself is free, but that does not mean the server, storage, bandwidth and transcoding are free. This article only discusses managing and playing media files that you or your team have the legal right to use.
Jellyfin is suitable for deployment on a VPS when:
- The media files are already on the VPS, a locally mounted disk or object storage;
- You mainly use Direct Play and do not need frequent video transcoding;
- Family members are in different regions and need remote access;
- The VPS bandwidth and monthly traffic are sufficient;
- You can accept maintaining your own domain, HTTPS, backups and client compatibility.
The following situations are better suited to a home server, NAS or a dedicated server with a GPU:
- All the media is on hard drives at home;
- You often play 4K HDR, HEVC, AV1 or high-bitrate remuxes;
- Multiple users will play at the same time;
- Client format support is poor and real-time transcoding is needed;
- The VPS only has a shared CPU, low traffic and a small disk.
If it is just for personal viewing, prioritize Direct Play on the client. Transcoding is the most resource-intensive part of Jellyfin and the place where an ordinary VPS is most likely to choke.
| Playback method | Server workload | Image quality | Applicable conditions |
|---|---|---|---|
| Direct Play | Lowest | Original quality | The client supports the video, audio and subtitle formats |
| Direct Stream | Lower | Usually no video re-encoding | Only the container needs to be changed |
| Transcode | Highest | Depends on settings | The client does not support the codec, the bitrate is too high, or subtitles need to be burned in |
For example, when the client supports H.264, AAC and MP4, the server can often Direct Play. If the video is HEVC and the audio is TrueHD, and the TV or browser does not support them, transcoding may be triggered. External SRT subtitles are usually fairly light, but ASS styled subtitles, image-based subtitles or subtitle burn-in may cause the server to re-encode the entire video.
Before buying a VPS, do not just look at the number of CPU cores. First check which codecs your common clients support, then decide whether you need a GPU.
| Use case | Recommended configuration | Notes |
|---|---|---|
| Single-user 1080p Direct Play | 2 cores, 2GB, 40GB SSD | Usable when media is on external storage |
| 2–3 users Direct Play | 4 cores, 4GB, 80GB NVMe | Focus on bandwidth and monthly traffic |
| Occasional 1080p software transcoding | 4–8 high-frequency CPU cores, 8GB | Shared-CPU VPSs are not necessarily stable |
| 4K, HDR or multiple transcoding streams | Dedicated GPU, 16GB+ RAM | Prefer a dedicated server or GPU instance |
| Large local media library | 4 cores, 8GB, disk sized to capacity | Put cache and metadata on SSD |
The Jellyfin database, posters, subtitles, thumbnails and transcoding cache are best placed on NVMe. Media files can go on large-capacity HDDs, block storage, a NAS or object storage, but the latency and egress costs of remote storage directly affect playback.
If you are still unsure what 2 cores and 4GB can run, you can first read How to Choose a VPS Configuration. When the media library is very large, also refer to Does a VPS Need Object Storage?.
Playback bandwidth is roughly equal to the average video bitrate. A 10 Mbps video played continuously for 1 hour consumes approximately:
10 Mbps × 3600 秒 ÷ 8 ≈ 4.5 GB
Playing 3 hours a day comes to about:
4.5 GB × 3 × 30 ≈ 405 GB
This does not yet count audio, subtitles, posters, repeated seeking and other users. If two people play at the same time, both the instantaneous bandwidth and the monthly traffic add up. High-bitrate 4K video can reach 40–80 Mbps, and a VPS nominally rated at 100 Mbps will quickly approach its limit.
"Unlimited traffic" may also come with fair-use, shared-bandwidth or throttling clauses. Read the provider's policy carefully before going live, and set up VPS traffic monitoring and overage alerts.
It is recommended to use Ubuntu 22.04/24.04 LTS or Debian 12. First confirm Docker is available:
docker version
docker compose version
Create the Jellyfin directories:
sudo mkdir -p /opt/jellyfin/config
sudo mkdir -p /opt/jellyfin/cache
sudo mkdir -p /srv/media/movies
sudo mkdir -p /srv/media/tv
sudo chown -R "$USER":"$USER" /opt/jellyfin
sudo chmod -R 750 /opt/jellyfin
The media directory can be provided by an upload tool, SFTP, rclone or a separate storage mount. Do not mount the entire root directory, the SSH configuration or other users' home directories into Jellyfin.
The official container example uses jellyfin/jellyfin and maps /config, /cache and the media directory. This article pins the current stable version 10.11.11 and makes the web port listen only on localhost:
services:
jellyfin:
image: jellyfin/jellyfin:10.11.11
container_name: jellyfin
restart: unless-stopped
environment:
TZ: Asia/Shanghai
JELLYFIN_PublishedServerUrl: https://media.example.com
volumes:
- /opt/jellyfin/config:/config
- /opt/jellyfin/cache:/cache
- /srv/media/movies:/media/movies:ro
- /srv/media/tv:/media/tv:ro
ports:
- "127.0.0.1:8096:8096/tcp"
Save it as /opt/jellyfin/compose.yaml, then start it:
cd /opt/jellyfin
docker compose up -d
docker compose ps
docker compose logs --tail=100 jellyfin
Test the local service:
curl -I http://127.0.0.1:8096
The official example also includes UDP 7359 for LAN auto-discovery. A public VPS usually does not need this port open; it is clearer for clients to connect directly using the HTTPS domain.
The media directory is mounted read-only with :ro, which prevents Jellyfin or plugins from accidentally deleting the original files. Only consider a read-write mount when you need to delete files from the admin interface; for production, it is better to maintain the media library through a separate file management process.
Prepare the domain media.example.com and point its A record to the VPS. Taking Caddy as an example:
media.example.com {
reverse_proxy 127.0.0.1:8096
}
Reload and check:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -I https://media.example.com
Jellyfin transmits login credentials, playback records and media content, so it should not be accessed over plaintext HTTP on the public internet. For the complete Caddy, DNS and certificate process, see The Complete Guide to Using Caddy as a Reverse Proxy on a VPS.
If you put Cloudflare in front, pay attention to single-upload limits, caching, proxied traffic and the terms of service. Jellyfin's dynamic video streams are usually not suitable for caching as ordinary static CDN content. Do not casually cache private media or circumvent platform rules just to "hide the IP".
After visiting https://media.example.com, complete the language, administrator, media library and remote access settings following the wizard.
- Do not use
admin, the domain or the email prefix as the username; - Set a long random password;
- Use the administrator only for configuration, and use a normal account for daily playback;
- Do not reuse passwords from the VPS, email or cloud provider accounts;
- Do not allow guest accounts to access admin settings.
Create separate media libraries for movies and TV shows, choosing the paths respectively:
/media/movies
/media/tv
Do not cram everything into one directory, otherwise the scraper will easily confuse movies, episodes, extras and subtitles.
Set the preferred metadata language and country/region, then enable the metadata sources you need. Metadata scraping downloads posters, backgrounds and cast information, which noticeably consumes /config and the cache directory when the file count is large.
Consistent naming saves more time than repeatedly matching by hand.
Example movie directory:
/srv/media/movies/
└── Movie Name (2025)/
├── Movie Name (2025).mkv
├── Movie Name (2025).zh-CN.srt
└── poster.jpg
Example TV show directory:
/srv/media/tv/
└── Series Name (2025)/
└── Season 01/
├── Series Name S01E01.mkv
├── Series Name S01E01.zh-CN.srt
└── Series Name S01E02.mkv
Writing the year, season number and episode number clearly reduces misidentification of same-name films and specials. Before batch renaming, keep a list of the original files to avoid a rule error scrambling the whole library.
Jellyfin supports external and embedded subtitles. To reduce transcoding:
- Prefer SRT, which clients support well;
- Name the subtitle file the same as the video;
- Use language tags such as
.zh-CN.srt,.en.srt; - Avoid setting complex ASS styled subtitles you do not need as the default;
- Image-based subtitles and subtitle burn-in significantly increase transcoding load;
- Font files should be placed in a clearly defined read-only directory.
If you have custom fonts, you can add a mount:
volumes:
- /srv/fonts:/usr/local/share/fonts/custom:ro
The font source must be legal; missing fonts cause ASS subtitle styles to render incorrectly or fall back.
Jellyfin officially supports Intel QSV, NVIDIA NVENC/NVDEC, AMD VA-API/AMF, Apple VideoToolbox and Rockchip RKMPP. The official Docker image includes jellyfin-ffmpeg; prefer the FFmpeg in the official image and do not casually replace it with other builds.
The virtual display adapter shown in many VPS control panels only drives the console display and cannot be used for video transcoding. Check first:
lspci -nn | grep -Ei "3d|display|vga"
ls -l /dev/dri
Without /dev/dri, NVIDIA devices or explicit GPU passthrough, you cannot assume hardware transcoding is available.
A dedicated server with an Intel iGPU can map /dev/dri into the container:
devices:
- /dev/dri:/dev/dri
The host user, container user and render/video group permissions must match. After a successful mapping, select QSV or VA-API in the Jellyfin admin panel and test the actual playback logs.
NVIDIA requires the correct host driver and the NVIDIA Container Toolkit. Enable NVENC only after nvidia-smi works normally and the container can see the GPU. Drivers, container runtime and GPU session limits all affect availability.
Play a test file that definitely requires transcoding, check the playback method and transcoding reason in the Jellyfin Dashboard, and observe at the same time:
docker stats jellyfin
nvidia-smi
Intel/AMD can use the corresponding GPU monitoring tools. High CPU usage does not necessarily mean complete failure, because subtitle burn-in, audio conversion and some filters may still fall to the CPU.
The following practices are more effective than upgrading the VPS:
- Use clients that support HEVC, AV1, HDR and common audio formats;
- Prepare H.264/AAC-compatible versions for browsers;
- Turn off subtitle burn-in you do not need;
- Set playback quality to original;
- Pre-transcode frequently watched content in advance;
- Avoid running multiple software transcodes at the same time on a shared-CPU VPS;
- Put the transcoding cache on SSD.
Direct Play simultaneously reduces CPU, latency and quality loss, and is the most worthwhile target to optimize first for remote Jellyfin.
Create a separate account for each family member and grant access by media library. Do not share the administrator account.
Recommended settings:
- Prohibit normal users from deleting media;
- Restrict which media libraries can be accessed;
- Set content ratings for children's accounts;
- Limit the remote playback bitrate;
- Disable download permissions that are not used;
- Regularly clean up devices and sessions that have not been used for a long time;
- Set reverse proxy rate limiting or a VPN for public internet logins.
If only you use it, you can access it after entering a private network via WireGuard, Tailscale or Headscale. There is already a tutorial on deploying Headscale on a VPS, suitable for unified management of multiple devices.
The advantage is simple paths and low latency. The disadvantage is that large-capacity SSDs are expensive, and when the whole machine fails, the media and the service may be lost together. Suitable for a small curated library, not for unlimited growth.
Block storage is used like a local disk, but is usually billed by capacity. Confirm IOPS, throughput, snapshots and cross-region recovery capabilities. When a network disk disconnects, Jellyfin may scan an empty directory; do not set up automatic deletion of original files.
Object storage is not a standard file system. Mounting it directly may run into random read, caching and request cost issues. Heavy seeking generates range requests, and the playback experience depends on the mount layer and the network.
OpenList can aggregate cloud drives, but then having Jellyfin read through WebDAV adds another layer of network and failure points. It can be used for personal testing; for a long-term media library, prefer stable local mounts, a NAS, rclone cache or a directly supported storage solution. You can also refer to the tutorial on deploying OpenList on a VPS.
Media files are usually very large, while Jellyfin configuration data is not. Back up at least:
/opt/jellyfin/config;compose.yaml;- Caddy/Nginx configuration;
- Users, playback records and plugin configuration;
- Custom posters, subtitles and NFO;
- A list of media files;
- A separate backup strategy for the media files themselves.
The cache directory can be rebuilt and usually does not need long-term backup. Media files should not be kept only on the same VPS; keep at least a second copy or a source from which they can be re-obtained.
Before upgrading, record the current image and back up the configuration:
cd /opt/jellyfin
docker inspect jellyfin --format '{{.Config.Image}}'
docker compose config > compose.before-upgrade.yaml
sudo tar -czf jellyfin-config-$(date +%F).tar.gz config
Copy the backup to another machine or object storage, then change the image version and rebuild:
docker compose pull
docker compose up -d
docker compose logs --tail=100 jellyfin
After upgrading, check login, media libraries, plugins, subtitles, client connections and one actual playback. If the database has already been migrated, simply changing the image back to the old version may not roll back; you also need to restore the pre-upgrade config backup.
First confirm the Jellyfin local port:
docker compose ps
curl -I http://127.0.0.1:8096
If the local port is fine but HTTPS returns 502, it is usually a wrong reverse proxy upstream address, port or Docker network.
Check the mount path and the directory inside the container:
docker exec jellyfin ls -la /media/movies
docker exec jellyfin ls -la /media/tv
If the host has the files but the container is empty, it is usually a Compose path, mount timing or permission issue.
Check in the Dashboard whether it is Direct Play or Transcode, then check the CPU, GPU, disk, upstream storage and public bandwidth. Do not judge based only on the CPU graph on the home page.
Switch to SRT subtitles or turn off subtitles to test. If it becomes Direct Play after turning them off, the original subtitle format or client compatibility triggered burn-in.
Check whether the client supports HDR, whether the TV has the correct mode enabled, and whether the video triggers tone mapping. HDR to SDR has high requirements for the GPU, driver and FFmpeg, and is not suitable for blindly enabling on an unverified ordinary VPS.
Check /opt/jellyfin/cache, confirm whether playback sessions were abnormally interrupted, and set a disk alert for the cache directory. Do not directly delete transcoding segments that are in use.
- Only manage and play media you have the legal right to use;
- Expose Jellyfin only via HTTPS or a VPN;
- Keep administrator and normal playback accounts separate;
- Mount media directories read-only by default;
- Do not publicly share the entire media library;
- Do not install plugins of unknown origin;
- Regularly update Jellyfin, Docker and the host;
- Configure login rate limiting, traffic alerts and disk alerts;
- Back up config, the reverse proxy and the media list;
- Pin the version and make a restore point before upgrading;
- Do not use the server to circumvent regional, copyright or platform access restrictions;
- Read the VPS provider's policies on high traffic, copyright complaints and abuse.
For a first deployment, you can start with a VPS with 2 cores, 4GB, 80GB NVMe and more than 2TB of monthly traffic, testing only one 1080p media library and one client. Make sure the playback status shows Direct Play, then gradually add users, subtitles and external storage.
If you transcode often, stop piling on shared CPU. Prioritize improving client compatibility, or switch to a dedicated server with an Intel iGPU or NVIDIA GPU. For 4K HDR and multi-user use, a home server or dedicated server with a GPU is usually more stable than an ordinary VPS.
The best Jellyfin experience is not "infinitely powerful server transcoding", but matching media formats, clients, storage and network as closely as possible. Get Direct Play working first, then talk about remote sharing and large media libraries.