SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-26

Jellyfin for VPS: stream your own films without wahala

Set up Jellyfin with Docker on a VPS, mount block storage, fix media permissions, and avoid CPU transcoding when direct play fit stream your library cleanly.

Wetin you dey build

Jellyfin media server for VPS: one container, three volumes, and block-storage disk wey hold your films and shows, so you fit reach am from any browser or Jellyfin app. The install na fifteen-line compose file. Everything wey fit go wrong afterwards dey come from two places: file permissions wey container no fit read, and asking GPU-less VPS to transcode video wey e no get business transcoding. This guide spend most of the length on these two, because na there support tickets dey come from.

Jellyfin free and fully open source. E no need account, e get no paywalled features, and e get no telemetry. Na why e dey almost every list of things wey worth self-hosting for 2026. E dey play media wey you own. E no ship content, and this guide no dey talk about acquiring any.

The transcoding reality, before you rent anything

Read this first, because e go change wetin you buy. Media server dey do one of two things when you press play. Direct play dey stream the file as e be: VPS dey read bytes from disk and push dem through network, so e dey use almost no CPU. Transcoding dey re-encode video as e dey play, with new resolution, new codec, or subtitles burned into the video, and na pure CPU work be that.

Typical VPS no get GPU. So every transcode dey run for CPU with libx264/libx265, and software encoding dey cost plenty resources. One 1080p H.264 transcode fit use up several shared vCPUs completely; 4K or HEVC transcode usually no fit maintain real-time speed at all, so playback dey stop and buffer forever. Hardware transcoding, wey make this work cheap for home box with Intel iGPU or Nvidia card, no dey available to you unless your provider rent GPU instances.

So the whole plan 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. Choose client apps wey support direct play: native Jellyfin apps for Android TV, iOS and Roku, plus Infuse, Kodi, and desktop Jellyfin Media Player. If you do this, VPS no go need touch ffmpeg, and modest 2 vCPU box fit stream to several people at once. If you plan to transcode, you need much bigger and more expensive box, and even then 4K no be good choice.

Do bandwidth math too, because na the other surprise. Direct play dey send the file at e own bitrate. Compressed 1080p file dey use 8-12 Mbps; 1080p Blu-ray remux dey use 20-30 Mbps; 4K HDR dey use 40-80 Mbps. Three people wey dey direct-play 10 Mbps files go use 30 Mbps sustained upload from your VPS. Check two numbers for your plan: port speed (e fit push 30 Mbps upstream?) and monthly transfer cap. One two-hour 10 Mbps film na about 9 GB out, so metered 1 TB/month allowance na small pass one hundred such films every month, three or four per day. Household wey dey watch 4K, with bitrate wey be four to eight times higher, go drain am much faster. Count anything else wey dey push traffic out from the same box against that same budget, including self-hosted RustDesk relay, wey carries complete remote desktop session whenever two peers no fit connect directly.

Prerequisites

  • Ubuntu 24.04 KVM VPS wey fresh, with root or sudo, plus Docker and Compose plugin installed.
  • Block-storage volume for the media, wey size match your library (see sizing below). The small root disk wey come with VPS no be where your films go dey.
  • Domain name if you want public HTTPS access, or WireGuard VPN for the same VPS if you prefer make everything remain private.
  • Media wey you get legal right to stream, your own rips, your own recordings, and files wey belong to you.

Mount 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, never 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 device

Mount am with UUID, no be with /dev/sdb. Device letters fit change order across reboots, and you fit end up formatting or mounting the wrong disk. Add one line to /etc/fstab:

UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  /mnt/media  ext4  defaults,nofail  0  2
sudo mount -a
df -h /mnt/media

nofail matter. Without am, if dem ever detach the block volume, the box no go boot and e go enter emergency shell. The biggest mistake for here na to run mkfs.ext4 on volume wey already get data, because e go wipe am. Format only new volumes. If the disk already get your library, skip go the fstab line.

Jellyfin dey expect make you arrange media like this

Jellyfin dey match metadata by folder and file names. If the layout no correct, films go show as files without title or poster, or episode fit match wrong series. Rules dey four: every movie must dey inside im own Name (Year) folder and filename must match; name season folders Season 01, no be S01; episode files must use S01E01; specials must go 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.mkv

The (Year) for movie name no be decoration. E dey help separate remakes so matcher fit choose the correct title. Keep Movies and Shows as separate top-level folders because each one go become Jellyfin library for one specific content type. If you mix dem, metadata provider fit get confused. Jellyfin fit index third folder for photos too, but the experience no strong like purpose-built photo server. So if your albums matter, give dem their own box wey dey run PhotoPrism or Immich, and leave this one for films and TV.

Permissions: number-one reason libraries dey come up empty

Na this wrong belief dey make people waste one evening. The official jellyfin/jellyfin image no honour PUID/PGID environment variables; na LinuxServer.io image (lscr.io/linuxserver/jellyfin) get dem. For the official image, you control the user with user: key for compose. If you no include am, the container go run as root. Any one wey you use, the rule still be the same: the uid/gid wey the container run as must fit read and traverse every media directory.

We go run as uid/gid 1000, wey be the first non-root user for standard Ubuntu box. Confirm your own 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 ~/jellyfin

Directories need execute bit (the x for 755), no be read only. Without am, the container no fit enter the folder, even though e fit list the name. The trap wey dey empty complete library na the parent directory: if the container's uid no fit traverse the mount itself, e no go reach /media/Movies or /media/Shows, and every library go come up empty at once with Access to the path ... is denied for the log. If e no fit read any single media folder, e go log am and skip am. So files wey you copy in as root fit silently disappear from the library. Na why we dey chown recursively and set execute bit for every directory, instead of fixing 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.com

Line by line: user: "1000:1000" na wetin really set file permissions, based on the ownership we set above. /config hold the whole server, accounts, libraries, metadata, and watch state, so e must be writable, and na this one you go back up. /cache na temporary working space. The media mount na :ro (read-only) on purpose: Jellyfin normally dey store artwork and metadata under /config, so e no ever need write to your library. Read-only also protect your files from accidental delete or bad plugin. We bind the port to 127.0.0.1 deliberately. Jellyfin web login na plain HTTP, so we no publish 8096 to public internet. JELLYFIN_PublishedServerUrl na the address server dey advertise for local autodiscovery through LAN UDP broadcast. Clients across internet no go see am; dem go simply use the URL wey you enter for the app. Set am to the address wey clients suppose receive, and expect to enter that URL by hand for remote devices.

Bring am up from the compose directory:

docker compose up -d
docker logs -f jellyfin

First run: setup wizard and your libraries

Because the port bind to localhost, use SSH tunnel from your laptop reach the wizard instead of opening hole for firewall:

ssh -L 8096:127.0.0.1:8096 you@your-vps-ip

Now open http://localhost:8096 for browser. The wizard go guide you through language, then create admin user with strong password. This account na your server account, so no use throwaway password again. Add your first library: choose content type Movies, point am to /media/Movies (the path inside the container, no be the host path), then repeat with Shows for /media/Shows. Finish, and Jellyfin go scan am. Correct result na posters and titles dey show within one or two minutes for small library. Later, you fit add or edit libraries under Dashboard → Libraries, and force new scan with Scan All Libraries.

If you dey use any transcoding at all, open Dashboard → Playback → Transcoding and set transcode temp path to /cache/transcodes so the temporary files go cache volume instead of making /config too big. Leave hardware acceleration as None, because no GPU dey available to accelerate am.

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 suppose avoid. The unsafe way na to publish port 8096 straight to internet: login details go travel as cleartext, and brute-force attempts fit start within hours.

Option A, TLS reverse proxy. Put Jellyfin for a 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 make you specify dem, and e need HTTP/1.1 to the upstream, otherwise 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 go use the address wey you give dem, and add fail2ban to slow brute-force attempts against the login. Once the server become public, point Uptime Kuma to the URL so you go hear about downtime before your viewers do.

Option B, keep it private on a VPN. No publish 8096 at all; reach Jellyfin only through a WireGuard tunnel wey terminate for the same box. For house use, na the simplest secure choice: no certificate, no public exposure, and no brute-force surface. Bind the container to the tunnel address or localhost, then connect through the VPN. See the WireGuard VPN setup for a private VPS for the tunnel itself.

Storage sizing and backups

File count no be the main thing for budget; quality na wetin matter. Compressed 1080p films dey take 4-15 GB each; 1080p remux dey take 20-40 GB; one season of 1080p TV dey take 15-40 GB; anything for 4K dey take 40-100 GB per film. Library wey get some hundred films plus some shows need 2-4 TB volume. E cheaper to provision block volume pass wetin you need once than migrate am later.

/config na the complete server state, so na the one thing wey you must back up. Take snapshot or stop the service and use tar on am, then keep the copy outside the server:

docker compose down
sudo tar czf jellyfin-config-$(date +%F).tgz -C ~/jellyfin config
docker compose up -d

/cache and the transcode folder no be important for backup; you fit recreate dem. You fit back up the media for /mnt/media separately, or accept say you fit rip am again. Most people choose the second option because of the size. Upgrades dey docker compose pull && docker compose up -d; the :10 tag above stay within the 10.x major version. Moving go the next major version na deliberate tag edit. Read the Jellyfin release notes before you do am, because library schema migration dey happen for major versions. One pinned tag plus one backed-up state directory na the complete recipe for any always-on container. Na the same pattern wey dey behind keeping self-hosted agent memory and schedules alive across reboots.

Failure modes, with the strings you go see

Library dey empty after scan. Log wey dey Dashboard → Logs (or ~/jellyfin/config/log/log_*.log) show:

System.UnauthorizedAccessException: Access to the path '/media/Movies' is denied.

The container uid no fit read that path. Cause fit be say root own the media, or another uid apart from your user: value own am; directory fit dey miss execute bit; or the parent mount itself no dey traversable by that uid. Fix am like this: chown -R 1000:1000 /mnt/media, directories 755, files 644, then scan again.

Playback dey pin CPU and buffer. docker stats jellyfin show CPU near 100% times your core count, and Dashboard → Playback list the session as Transcode with speed below 1.0x. The client no dey direct-play, so the VPS dey use CPU to transcode slower than real time and e dey fall behind. Cause fit be unsupported codec or container, subtitle burn-in, or HDR tone-mapping. Fix am by switching to direct-play client, keeping sources for H.264/AAC, using text subtitles (SRT) instead of image subtitles (PGS/VOBSUB), wey dey force burn-in, and keeping 4K HDR away from CPU-only box entirely.

"No compatible streams are available." The full message normally be "This client isn't compatible with the media and the server isn't sending a compatible media format." The client reject the source, and the fallback transcode too fail to start. Cause fit be broken ffmpeg command, unreadable file, or the user profile blocking video conversion. Fix am by reading the ffmpeg line for Dashboard → Logs, confirming say the file dey play at all, checking the user's playback permissions if you depend on transcoding, and trying another client to rule out browser codec problems.

Films no get poster or the wrong one. Metadata no match. Cause fit be movie wey no dey inside im own Name (Year) folder, season folder named S01 instead of Season 01, episodes no dey for S01E01 form, or year missing. Fix am by renaming to the layout above, then Refresh metadata → Replace all, or use Identify for one item to select the correct TMDB/TVDB entry.

FAQ

VPS fit transcode video without GPU?

Yes, but na CPU only, and e costly. One 1080p software transcode fit use up several vCPUs, and 4K or HEVC usually no fit keep up with real time, so playback go buffer. The better move na to avoid transcoding: keep your library for H.264/AAC and use client apps wey support direct-play, so na VPS just dey stream bytes. Rent GPU instance only if you truly need on-the-fly transcoding.

Why my Jellyfin library empty after scan?

Na almost always permissions cause am. The official jellyfin/jellyfin image dey run as any user: wey you set (or root), and if the files no readable by that uid, scan logs Access to the path ... is denied and skip dem. Fix ownership with chown -R 1000:1000 /mnt/media, give directories execute bit (755), then scan again. Check the parent directory too, because if the container uid no fit traverse /mnt/media itself, e no go reach the library folders and everything go remain empty. The second common cause 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 for a subdomain, so login and stream dey encrypted, and add fail2ban. Never expose plain port 8096, because e dey send your password as cleartext. Or keep am completely private and access am only through VPN, wey na the simplest safe choice for household. No give the apps the public address directly; autodiscovery na local-network broadcast, so e no reach clients wey dey connect through internet.

How much disk and bandwidth Jellyfin VPS need?

Disk depend on quality: budget 4-15 GB for each compressed 1080p film, 20-40 GB for each remux, and 40-100 GB for 4K. So, most libraries go need 2-4 TB block volume. Bandwidth depend on direct-play bitrate: 8-12 Mbps for each 1080p stream, and much more for 4K. Confirm say your port speed fit handle the number of simultaneous viewers, and monitor the monthly transfer cap. Add CPU headroom if you plan to transcode. Prioritise bandwidth over cores if you plan to use direct-play.

Jellyfin itself na free, open-source software, and running am completely legal. Wetin matter na the content: stream only media wey you own or get licence to keep, like your own disc rips, recordings, or files wey you get right to use. Jellyfin no ship any media and e no provide way to obtain any one; na player for library wey you already own.