how to run jellyfin on vps with docker
Learn how to set up Jellyfin for media streaming on a VPS. We cover Docker volumes, file permissions, and why CPU transcoding fit kill your server.
Wetin you dey build
Jellyfin media server for VPS: one container, three volumes, and one block-storage disk wey go hold your films and shows. You fit access am from any browser or Jellyfin app. The install na just fifteen-line compose file. If anything go wrong after, e go come from two things — file permissions wey container no fit read, and when you ask VPS wey no get GPU to transcode video wey e no suppose transcode. This guide go focus mostly on those two things, because na dem dey cause most wahala.
Jellyfin na free and fully open source. No account, no paywall, and no telemetry — na why e dey for almost every list of things worth self-hosting in 2026. E dey play media wey you own. E no dey carry any content, and this guide no be for how to find content.
Wetin transcoding be, before you rent anything
Read dis one first, because e go change wetin you go buy. Media server dey do one of two things when you press play. Direct play dey stream file exactly as e dey: VPS go read bytes from disk and push dem down the wire, e no go use much CPU. Transcoding dey re-encode video on the fly — new resolution, new codec, or when dem burn subtitles inside — and dat one na pure CPU work.
Typical VPS no get GPU. So every transcode go run for CPU with libx264/libx265, and software encoding dey cost plenty. One single 1080p H.264 transcode fit use up several shared vCPUs; 4K or HEVC transcode no dey fit keep up with real time at all, so playback go stop and buffer forever. Hardware transcoding — di thing wey dey make am cheap for home box wey get Intel iGPU or Nvidia card — no dey available for you unless your provider dey rent GPU instances.
So di whole strategy for VPS na: avoid transcoding. Keep your library for codecs wey your clients fit play natively — H.264 video, AAC or AC3 audio, inside MP4 or MKV container — and pick client apps wey dey direct-play: di native Jellyfin apps for Android TV, iOS and Roku, plus Infuse, Kodi, and di desktop Jellyfin Media Player. If you do dat, VPS no go ever touch ffmpeg, and one small 2 vCPU box fit stream to several people at once. If you plan to transcode, you go need bigger and expensive box, and even den, 4K na bad bet.
Calculate your bandwidth too, because dat one na di second surprise. Direct play dey send file with im own bitrate. One compressed 1080p file dey run 8-12 Mbps; 1080p Blu-ray remux dey run 20-30 Mbps; 4K HDR dey run 40-80 Mbps. Three people wey dey direct-play 10 Mbps files na 30 Mbps sustained upload from your VPS. Check two numbers for your plan: di port speed (e fit push 30 Mbps upstream?) and di monthly transfer cap. One two-hour 10 Mbps film na about 9 GB out, so 1 TB/month allowance na more than hundred films like dat every month — three or four every day — and if household dey watch 4K, wey na four to eight times di bitrate, dem go finish am much faster.
Prerequisites
- Fresh Ubuntu 24.04 KVM VPS wey get root or sudo access, and Docker plus Compose plugin must dey installed.
- Block-storage volume for your media, make e big enough for your library (see sizing below). No use the small root disk wey VPS dey come with for your films.
- Domain name if you want public HTTPS access, or WireGuard VPN on the same VPS if you want keep everything private.
- Media wey you get legal right to stream — your own rips, your own recordings, or files wey you own.
Mount the block storage first
Attach the volume for your provider panel, then find am and mount am. Get the device name from lsblk — e go be something like /dev/sdb or /dev/vdb, e no go ever be the 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 am by UUID, no use /dev/sdb, because device letters dey change after reboot and you fit format or mount wrong disk. Add one line for /etc/fstab:
UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /mnt/media ext4 defaults,nofail 0 2sudo mount -a
df -h /mnt/medianofail dey matter: if you no use am, if dem detach the block volume, the box no go boot and e go drop you for emergency shell. The biggest mistake wey person fit make for here na to run mkfs.ext4 for volume wey don get data — e go wipe everything. Format new volumes only; if the disk don get your library for am already, skip straight to the fstab line.
Arrange your media exactly how Jellyfin want am
Jellyfin dey use folder and file names take find metadata. If you arrange am wrong, films go show as untitled files without poster, or one episode fit match wrong series. Three rules dey: every movie must dey inside its own Name (Year) folder with correct filename; season folders must carry name Season 01, no be S01; episode files must use S01E01; and specials must dey inside 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.mkvThe (Year) wey dey for movies no be for decoration — e dey help Jellyfin know difference between remakes so e fit grab correct title. Keep Movies and Shows as separate top-level folders because each one go become Jellyfin library for specific content type, and if you mix dem, metadata provider go confuse.
Permissions: the main reason why libraries dey come up empty
Dis na one misconception wey dey make people waste whole evening. Di official jellyfin/jellyfin image no dey use PUID/PGID environment variables — dem na for di LinuxServer.io image (lscr.io/linuxserver/jellyfin). For di official image, you go use user: key for compose to control di user, and if you no put am, di container go run as root. No matter which one you use, di rule na di same: di uid/gid wey di container dey run with must get power to read and traverse every media directory.
We go run as uid/gid 1000, wey na di first non-root user for standard Ubuntu box. Check yours and set 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 ~/jellyfinDirectories need di execute bit (di x for 755), no be just read — if you no get am, di container no fit enter di folder even if e fit see di name. Di trap wey dey make whole library vanish na di parent folder: if di container uid no fit traverse di mount itself, e no go ever reach /media/Movies or /media/Shows, and every library go come up empty at once with Access to the path ... is denied for di log. Any single media folder wey e no fit read, e go log am and skip am, so if you copy batch of files as root, dem go just vanish from di library without noise. Na why we dey use chown recursively and set di execute bit for every directory instead of to fix only one folder.
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.comLine by line: user: "1000:1000" na him dey set file permissions, e follow the ownership wey dey above. /cache na throwaway working space. The media mount na :ro (read-only) by design: Jellyfin dey use /config for artwork and metadata by default, so e no need write for your library, and read-only dey protect your files from accidental delete or bad plugin. The port dey bind to 127.0.0.1 by design — Jellyfin web login na plain HTTP, so we no go ever publish 8096 to public internet. JELLYFIN_PublishedServerUrl na the address wey server dey use advertise for local autodiscovery — na LAN UDP broadcast, so clients for internet no go see am and dem go just use the URL wey you type for app. Set am to the address wey you wan tell clients, and prepare to type that URL manually for remote devices.
/config hold the whole server — accounts, libraries, metadata, and watch state — so e must be writable and na him you dey back up.
Bring am up from the compose directory:
docker compose up -d
docker logs -f jellyfinFirst run: setup wizard and your libraries
Because the port dey bound to localhost, use SSH tunnel from your laptop reach the wizard instead of to open firewall hole:
ssh -L 8096:127.0.0.1:8096 you@your-vps-ipNow browse to http://localhost:8096. The wizard go guide you through language, then create admin user with a strong password — this account na your server, so no use throwaway password. Add your first library: choose content type Movies, point am to /media/Movies (the path inside the container, no be the host path), and repeat for Shows at /media/Shows. Finish, and Jellyfin go start to scan. If everything correct, posters and titles go show within one or two minutes for small library. You fit add or edit libraries later under Dashboard → Libraries, and force am to rescan with Scan All Libraries.
If you dey use any transcoding, open Dashboard → Playback → Transcoding and set the transcode temp path to /cache/transcodes so the churn go land for cache volume instead of to bloat /config. Leave hardware acceleration set to None — no GPU dey to accelerate with.
Remote access: TLS reverse proxy, or keep it on the VPN
You get two safe ways to reach Jellyfin from outside, and one unsafe way wey you must avoid. Unsafe way na to publish port 8096 straight to the internet: the login go travel for cleartext and dem go brute-force the port within hours.
Option A — TLS reverse proxy. Put Jellyfin for subdomain behind Traefik with automatic TLS for your Docker apps, or behind nginx with a Let's Encrypt certificate issued by Certbot. Jellyfin dey use WebSockets for real-time updates, so the proxy must forward the upgrade headers. Traefik dey do this automatically; nginx need dem to dey clearly written, and e need HTTP/1.1 to the upstream or the upgrade no go happen:
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";
}Set JELLYFIN_PublishedServerUrl to the https:// address so any local autodiscovery go advertise the correct URL — remote apps dey use the address wey you give dem — and add fail2ban to slow brute-force attempts against the login. Once the server don public, point Uptime Kuma at the URL so you go hear say server don down before your viewers see am.
Option B — keep it private on a VPN. No publish 8096 at all; reach Jellyfin only through a WireGuard tunnel wey dey end for the same box. For household, this na the simplest secure choice — no certificate, no public exposure, no brute-force surface. Bind the container to the tunnel address or localhost and connect over the VPN. See the WireGuard VPN setup for a private VPS for the tunnel itself.
Storage sizing and backups
Plan your budget based on quality, no be file count. Compressed 1080p films dey run 4-15 GB for each one; 1080p remux dey run 20-40 GB; one season of 1080p TV dey run 15-40 GB; anything 4K dey run 40-100 GB per film. If you get library wey get few hundred films plus some shows, you go need 2-4 TB volume. E better make you buy more space for block volume once than to do migration later.
/config na the entire server state, so na that one you must back up. Use snapshot or stop-and-tar am, and keep the copy far from the box:
docker compose down
sudo tar czf jellyfin-config-$(date +%F).tgz -C ~/jellyfin config
docker compose up -d/cache and the transcode folder na disposable things. For the media wey dey /mnt/media, you fit back up am separately or just accept say you go re-rip am — most people dey do the second one because of the size. Upgrades na docker compose pull && docker compose up -d; the :10 tag wey dey above go stay within the 10.x major, so to move to the next major, you must edit the tag purposely — check Jellyfin release notes before you do am, because library schema migrations dey happen for major versions.
Failure modes, with the strings you will see
Library empty after scan. Log for Dashboard → Logs (or ~/jellyfin/config/log/log_*.log) show:
System.UnauthorizedAccessException: Access to the path '/media/Movies' is denied.Container uid no fit read that path. Cause: media belong to root or another uid wey different from your user: value, directory wey no get execute bit, or the parent mount wey that uid no fit pass through. Fix: chown -R 1000:1000 /mnt/media, directories 755, files 644, then rescan.
Playback dey pin CPU and buffers. docker stats jellyfin show CPU near 100% times your core count, and Dashboard → Playback list the session as Transcode with speed below 1.0x. Client no dey direct-play, so VPS dey do CPU-transcoding wey slow pass real time and e dey lose. Cause: unsupported codec or container, subtitle burn-in, or HDR tone-mapping. Fix: switch to direct-play client, keep sources for H.264/AAC, use text subtitles (SRT) instead of image subtitles (PGS/VOBSUB) wey dey force burn-in, and no use 4K HDR for CPU-only box at all.
"No compatible streams are available." Full message usually be "This client isn't compatible with the media and the server isn't sending a compatible media format." Client reject the source and fallback transcode no start. Cause: ffmpeg command don spoil, file no fit read, or user profile dey block video conversion. Fix: read ffmpeg line for Dashboard → Logs, confirm say file dey play, check user playback permissions if you dey use transcoding, and try another client to see if browser codec na the problem.
Films no get poster or wrong poster. Metadata no match. Cause: movie no dey inside im own Name (Year) folder, season folder name be S01 instead of Season 01, episodes no dey S01E01 form, or year missing. Fix: rename am to the layout wey dem list above, then Refresh metadata → Replace all, or use Identify for one single item to pick the correct TMDB/TVDB entry.
FAQ
VPS fit transcode video if e no get GPU?
E fit, but e go use CPU only, and e go cost plenty money. One single 1080p software transcode fit jam several vCPUs, and 4K or HEVC usually no fit keep up with real time, so playback go buffer. Best way na to avoid transcoding: keep your library for H.264/AAC and use client apps wey dey do direct-play, so the VPS go just stream bytes. Rent GPU instance only if you really need on-the-fly transcoding.
Why my Jellyfin library empty after I run scan?
Na almost always permissions. The official jellyfin/jellyfin image dey run as whatever user: you set (or root), and if the files no dey readable by that uid, the scan logs Access to the path ... is denied and skip dem. Fix ownership with chown -R 1000:1000 /mnt/media, give directories the execute bit (755), and rescan — and check the parent folder too, because if the container's uid no fit pass through /mnt/media itself, e no go reach the library folders and everything go empty. The second most common reason na folder layout wey no match wetin Jellyfin expect.
How I fit access Jellyfin remotely and safely?
Two good options dey. Put am behind TLS reverse proxy on a subdomain so say login and stream go get encryption, and add fail2ban — no ever expose plain port 8096, because e dey send your password for cleartext. Or keep am entirely private and access am only through VPN, na the simplest safe choice for house. Give the apps the public address directly — autodiscovery na local-network broadcast, so e no dey reach clients wey dey come through internet.
How much disk and bandwidth Jellyfin VPS need?
Disk depend on quality: budget 4-15 GB for every compressed 1080p film, 20-40 GB for every remux, and 40-100 GB for 4K, so most libraries need 2-4 TB block volume. Bandwidth depend on direct-play bitrate — 8-12 Mbps for every 1080p stream, and much more for 4K — so make sure say your port speed fit handle the number of people wey dey watch at the same time and watch your monthly transfer cap. Add CPU headroom if you plan to transcode; prioritize bandwidth over cores if you plan to do direct-play.
Na illegal to run Jellyfin on a VPS?
Jellyfin itself na free, open-source software and to run am dey legal. Wetin matter na the content: stream only media wey you own or wey you get license for — your own disc rips, recordings, or files wey you get right for. Jellyfin no dey carry any media and e no dey provide way to get any; e na player for library wey you already own.