Why Jellyfin in 2026
The personal media server landscape has changed in a few years. Plex raised the price of its Lifetime Pass in 2025, then tripled it on 1 July 2026 (from $249.99 to $749.99, an increase announced in May 2026), making entry expensive for family use. Emby, the other historical alternative, also runs on a freemium model — some key features such as hardware transcoding, DVR or offline downloads on mobile are reserved for Emby Premiere subscribers.
Jellyfin was born in 2018 as a fork of Emby, precisely to stay free. The GPLv2 licence forbids closing the code again — if a contributor reintroduces proprietary code, users can legally fork. That is a structural guarantee neither Plex nor Emby offers.
As for versions, the 12.x series has succeeded 10.11: 12.0 came out on 8 September 2026, and 12.4, published on 26 October 2026, is the latest stable release seen at the time of writing. This major version jump has a practical consequence: a database migration that can sometimes be very long, covered further down in this guide.
On features, Jellyfin covers nearly every mainstream use: adaptive streaming, software and hardware transcoding, native iOS, Android, Android TV and Roku apps, LiveTV and DVR recording, automatic subtitles, SSO authentication through OIDC and LDAP, and a plugin system that extends the application without touching the core.
What Jellyfin offers
- DLNA and HLS streaming — serves films, series and music to all your devices, including TVs without a native app.
- Official apps — iOS, Android, Android TV, Roku, Fire TV, Kodi; a responsive web interface for everything else.
- On-the-fly transcoding — converts the source format if the target device cannot play it, in software or through a GPU.
- LiveTV and DVR recording — plug in a USB tuner or an IPTV source to watch and record live television.
- OIDC and LDAP SSO — plug in your existing directory or an identity provider such as Authentik or Keycloak.
- Automatic subtitles — OpenSubtitles plugin, extraction of embedded tracks, WebVTT rendering.
- Extended plugins — LastFM integrations, scrobbling, music metadata, trailers, collection management.
- No proprietary cloud at all — no authentication through Plex servers, no playback data sent to a third party.
Sizing by use case
Needs vary considerably with what you plan to do.
SD and 1080p without transcoding (direct play): if your devices natively play the source codec (H.264 or H.265), Jellyfin does not transcode — it serves the file like a smart file server. 2 vCPU and 2 GB of RAM are plenty for a library of several hundred titles. This is the most common use case on a VPS.
1080p with software transcoding: when the client does not support the codec or when you cap the bitrate, Jellyfin encodes on the fly. A single 1080p H.264 stream uses 2 to 4 cores depending on the source resolution. Aim for 4 vCPU / 4 GB RAM minimum for two simultaneous users.
4K HDR with software transcoding: HDR tone-mapping (HDR→SDR conversion for screens without HDR) is extremely demanding. A single 4K HDR stream can saturate 8 cores. This use case does not suit a standard cloud VPS — prefer 4K direct play (the client must support HEVC/HDR) or a dedicated server with a GPU.
GPU hardware transcoding (VAAPI/NVENC): cuts CPU load sharply, but needs access to the /dev/dri device or an NVIDIA card. Standard cloud VPS do not expose a GPU; this option is reserved for dedicated servers or specialised GPU VPS.
Disk for a version upgrade: plan enough room to hold a full copy of the configuration folder (database included) next to the original. Jellyfin's backup documentation requires at least 5 GB free for a built-in backup, and more if you include subtitles and trickplay.
Install Jellyfin on an Ubuntu/Debian VPS with Docker Compose
Prepare the VPS and install Docker
Connect to your VPS over SSH (Ubuntu 22.04 or Debian 12). Update the system and install Docker:
apt update && apt upgrade -ycurl -fsSL https://get.docker.com | shsystemctl enable --now dockerCheck that Docker works:
docker info. Do this update now, on a server that is still empty: later, on a machine running a package-installed Jellyfin, anapt upgradecan restart the service at the worst moment (see the section on version upgrades).Create the folder tree
Choose where to keep the Jellyfin configuration and your media:
mkdir -p /opt/jellyfin/{config,cache}mkdir -p /mnt/media/{films,series,music}chown -R 1000:1000 /opt/jellyfin /mnt/mediaUID 1000 is the one the Compose file below gives the Jellyfin container (the
usersetting, optional according to the documentation: without it, the container runs as root).Write the docker-compose.yml file
Create
/opt/jellyfin/docker-compose.yml. Underservices:thenjellyfin:(YAML indentation of two spaces per level), put these lines:image: jellyfin/jellyfin:12.4container_name: jellyfinuser: "1000:1000"ports:then, below,- "127.0.0.1:8096:8096"restart: unless-stoppedvolumes:then, below,- /opt/jellyfin/config:/config,- /opt/jellyfin/cache:/cacheand- /mnt/media:/media:roenvironment:then, below,- JELLYFIN_PublishedServerUrl=https://jellyfin.your-domain.comDo not write
latest. According to the official documentation, that tag "always tracks the latest stable release, including through major and minor version bumps": a plaindocker compose pullis then enough to trigger a database migration you did not prepare. Pin a version — anX.Ytag (such as12.4) follows the fixes of that branch, anX.Y.Ztag freezes an exact release. Check the current tag on the image registry before copying12.4.The port is published on
127.0.0.1only: just nginx, on the same machine, reaches it, and Jellyfin cannot be reached in plain HTTP on the public IP.network_mode: hostis only needed for DLNA discovery, according to the documentation; DLNA only works on a local network, and on a VPS that mode would expose port 8096 on every interface.Start the container
cd /opt/jellyfindocker compose up -ddocker compose logs -fJellyfin listens on port 8096. Wait for the
Startup completeline in the logs before moving on.Configure nginx as a reverse proxy
Install nginx and certbot:
apt install -y nginx certbot python3-certbot-nginxCreate
/etc/nginx/sites-available/jellyfinwith a singleserverblock, on port 80:listen 80;,server_name jellyfin.your-domain.com;, then, insidelocation / {,proxy_pass http://localhost:8096;, the headersproxy_set_header Host $host;,proxy_set_header X-Real-IP $remote_addr;,proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;andproxy_set_header X-Forwarded-Proto $scheme;, then, for WebSockets,proxy_http_version 1.1;,proxy_set_header Upgrade $http_upgrade;andproxy_set_header Connection "upgrade";. Do not write a 443 block yet: itsssl_certificatelines would point to files that do not exist until certbot has run, and nginx would refuse to load the configuration.Enable the site, check it, then get the certificate:
ln -s /etc/nginx/sites-available/jellyfin /etc/nginx/sites-enabled/nginx -t && systemctl reload nginxcertbot --nginx -d jellyfin.your-domain.comCertbot's nginx plugin obtains the certificate, stores it in
/etc/letsencrypt/live/jellyfin.your-domain.com/(fullchain.pemandprivkey.pem), then installs it: it adds the 443 listener and thessl_certificateandssl_certificate_keylines to the block itself, along with the HTTP to HTTPS redirect, which is on by default. Check the result withnginx -t.If you prefer
certbot certonly --nginx -d jellyfin.your-domain.com, which obtains the certificate without touching the configuration, then write the 443 block yourself:listen 443 ssl;, the sameserver_name,ssl_certificate /etc/letsencrypt/live/jellyfin.your-domain.com/fullchain.pem;,ssl_certificate_key /etc/letsencrypt/live/jellyfin.your-domain.com/privkey.pem;and the samelocation /. Then replace the port 80 block'slocation /withreturn 301 https://$host$request_uri;, and runnginx -t && systemctl reload nginx.Initial setup wizard
Open
https://jellyfin.your-domain.comin your browser. The wizard walks you through:1. Creating the administrator account
2. Adding your library (point to/media/films,/media/series)
3. Choosing the language and the metadata providers (TheMovieDB for films, TheTVDB for series)Run a full scan once setup is done: Dashboard > Libraries > Scan All Libraries.
Plugins not found — check the repository
This is one of the most asked questions after a version upgrade: the plugin catalogue is empty, or every plugin shows "An error occurred while getting the plugin details from the repository", while the instance otherwise works.
What upstream says. GitHub issue #16654 ("No longer access plugins", reported on version 10.11.8) describes this error although the reporter's repository URLs were correct. It was closed without a fix ("not planned") by a bot after going stale. A contributor there suggests that some countries block raw.githubusercontent.com, which hosts several third-party plugin manifests; one user reports fixing their case by uninstalling the plugins, restarting, then reinstalling them. No single cause is established: this guide's earlier explanation, a URL change between two versions, is not what the issue says and has been removed.
Checks, in order:
1. Go to Dashboard > Plugins > Repositories. The official stable repository must point to https://repo.jellyfin.org/files/plugin/manifest.json. If you had switched to the "unstable" repository, go back to the stable one
2. From the VPS, check that the manifest answers: curl -sI https://repo.jellyfin.org/files/plugin/manifest.json. For a third-party repository hosted on GitHub, also test curl -sI https://raw.githubusercontent.com
3. Click "Save", then restart Jellyfin to force the metadata to reload: docker compose -f /opt/jellyfin/docker-compose.yml restart
4. As a last resort, uninstall the affected plugins, restart, reinstall them, restart again
Storage strategy for Jellyfin on a VPS
Storage is often the main constraint. Orders of magnitude to know (standard estimates):
- 1080p MKV H.264 film: 5 to 15 GB depending on length and bitrate
- 4K HDR H.265 film: 15 to 60 GB, even more for Blu-ray remasters
- 1080p series episode: 1 to 4 GB
Worked example: 100 1080p films × 8 GB (average) = 800 GB minimum. For a collection of 300 mixed films + 10 complete series, count 3 to 5 TB.
Option 1 — Additional VPS volume (recommended to start): most VPS providers offer block volumes (Block Storage) that can be attached live, billed per GB per month. Mount it as ext4 on /mnt/media and pass it as a bind mount to Docker. Advantage: direct I/O on a local disk, no network transit. Drawback: cost grows linearly with size.
After attaching the volume (here /dev/sdb):
mkfs.ext4 /dev/sdbmount /dev/sdb /mnt/mediaecho '/dev/sdb /mnt/media ext4 defaults 0 2' >> /etc/fstab
Option 2 — Hetzner Storage Box (CIFS/Samba): a hybrid solution — your VPS stays light, storage lives on the Storage Box. Mount it over CIFS:
apt install -y cifs-utilsinstall -m 600 /dev/null /root/.storagebox-credentials, then write two lines in it: username=uXXXXXX and password=…mount -t cifs //uXXXXXX.your-storagebox.de/backup /mnt/media -o seal,credentials=/root/.storagebox-credentials,uid=1000,gid=1000,iocharset=utf8
The credentials file keeps the password out of the shell history and the process list; the seal option encrypts the SMB connection, which is not necessarily encrypted by default. First enable SMB in the Hetzner Console, plus external reachability if your VPS is not at Hetzner.
Network latency is acceptable for pre-buffered streaming (Jellyfin reads ahead), but unsuited to live transcoding — CIFS throughput cannot keep up.
Option 3 — Local NAS with a Tailscale or Cloudflare Tunnel: if you already own a home NAS, you can expose your library to a remote VPS through an encrypted tunnel. Tailscale creates a virtual private network between your NAS and your VPS in under ten minutes. Streaming then flows NAS → VPS → user, with controlled latency. Keep this option for very large, already-built libraries.
Break-even point: compare the per-TB price of your block volume with that of a Storage Box on your provider's current price lists. Beyond 500 GB, external storage is often cheaper, provided you accept the network latency.
Hardware transcoding — VAAPI and NVENC
Software transcoding suits one or two simultaneous sessions on a good VPS. Beyond that, or for regular 4K HDR, hardware transcoding is essential.
VAAPI (Intel/AMD): on a dedicated server with an integrated Intel GPU or an AMD GPU, the /dev/dri/renderD128 device exposes the encoder. Add to the Docker service the lines devices: then - /dev/dri/renderD128:/dev/dri/renderD128, and group_add: with the ID of the host's render group (getent group render | cut -d: -f3).
Then enable hardware acceleration in Dashboard > Playback > Transcoding: choose QSV (Intel Quick Sync, preferred on recent Intel GPUs under Linux) or VA-API (AMD GPUs, older Intel GPUs).
NVENC (NVIDIA): needs the NVIDIA driver and the NVIDIA Container Toolkit on the host (the old nvidia-docker2 package is deprecated). Add to the service a deploy: > resources: > reservations: > devices: block containing - driver: nvidia, count: 1 and capabilities: [gpu].
Reminder: standard cloud VPS do not expose a GPU or a /dev/dri device. This mode is reserved for dedicated servers with a graphics card or a processor with an accessible integrated GPU.
Cap the load to avoid saturation: Jellyfin has no global limit on transcodes, but you can set, for each user, a maximum number of simultaneous sessions (Dashboard > Users > the user's profile) and throttle the bitrate of remote clients in the playback settings. On a 4 vCPU VPS, aim for 2 or 3 simultaneous transcodes at most — beyond that, other users see a noticeable degradation.
Moving to a major version: what Jellyfin 12 requires
Going from 10.11 to 12.x is not a routine update. The 12.0 release notes (8 September 2026) set the frame:
Upgrade paths. You can upgrade directly from 10.10.7 or any 10.11.x, with no intermediate step. A release older than 10.10.7 must first go through 10.10.7.
Backup. The notes "strongly" recommend a full backup of the data directory, because rolling back requires a full restore. The backup documentation confirms it: Jellyfin has no downgrade mechanism.
Duration. The migration checks the paths of every library item, which can take time depending on its size, and the first library scan will be much longer than usual.
Announced side effects. Alternative versions that were resolved automatically (not merged by hand) are removed and come back after a full scan; some films may appear as newly added; usernames that differ only by case must be fixed before the upgrade; the legacy /emby/* and /mediabrowser/* routes disappear, which can break older third-party clients; legacy authorisation is disabled; global subtitle settings are replaced by per-library settings.
These points come from the 12.0 notes and apply to 12.0; reread the notes of the version you are targeting.
A durable procedure to upgrade without losing your library
Read the release notes and check the upgrade path
Open the target version's page on Jellyfin's GitHub repository and read its "upgrade notes". Identify your current version (
docker compose imagesor the dashboard). If it is older than 10.10.7, go through 10.10.7 first. Remove plugins from third-party repositories before the migration, as the 12.0 notes ask.Prevent any automatic update
An interrupted migration is the documented cause of the worst case (see the next section). So disable everything that can restart or replace the service without you:
- Docker: pin the tag in
docker-compose.yml(neverlatest) and turn off Watchtower or any task that runsdocker compose pull
- Debian/Ubuntu packages: list the packages withdpkg -l | grep -i jellyfin, hold them withapt-mark hold <package>, and check withapt-mark showhold. A held package is no longer updated byapt upgradeorunattended-upgrades
- Also avoid runningapt upgradeon the machine while the migration runs, even for other packages, until you have confirmed how your distribution behavesStop Jellyfin and copy the data folder
A hot copy of an open database can be unusable: the documentation states that, for a manual backup, the server must be stopped, otherwise the database may be locked and unrecoverable on restore.
cd /opt/jellyfindocker compose stopdf -h /optanddu -sh /opt/jellyfin/configto check the spacecp -a /opt/jellyfin /opt/jellyfin.before-upgradeIf your configuration lives in a named volume (the case of the ServOrbit catalogue template, which mounts
jellyfin-dataon/config), copy the volume after checking its name withdocker volume ls:docker run --rm -v jellyfin-data:/source:ro -v /root/jellyfin-before:/copy alpine cp -a /source/. /copy/.The built-in backup (Dashboard > Backups) is an alternative, but it does not replace a folder copy when the migration itself is the risk.
Change the tag and start
Edit the
image:line ofdocker-compose.ymlto the target version, then:docker compose pulldocker compose up -dThe server runs migrations at startup, before accepting connections. While they run, the health check repeats "Server is still starting up."
Watch without touching anything
docker compose logs -fshows the running migration, numbered (for example "1 of 21"). In a second terminal,docker statsshows the load. Do not restart the container and do not rerun theupcommand while a migration is running: that is the gesture that blocked databases in issue #18066. Note thatrestart: unless-stoppedalso restarts a container that was killed (out of memory, for instance) mid-migration: if the log shows repeated starts, stop it yourself withdocker compose stopand diagnose before resuming.Decide: wait or restore
A slow migration uses CPU or disk; a blocked migration is inert. The reports in issue #18066 give orders of magnitude: one core at 100% for over two hours on a database of more than 3 GB before a fix existed, a full upgrade of about two hours for another user, a success after an overnight wait for a third; conversely, 0% CPU, no disk I/O and idle threads for those whose migration was really blocked. Wait as long as CPU or disk are working and the counter advances. If everything has been inert for a long time, or the log no longer moves, restore.
Restore the copy if the process is inert
docker compose downmv /opt/jellyfin /opt/jellyfin.blockedcp -a /opt/jellyfin.before-upgrade /opt/jellyfinPut the previous exact tag back in
docker-compose.yml, thendocker compose up -d. Do not try to downgrade the database in place: Jellyfin has no downgrade mechanism. When the upgrade succeeds, run a full library scan, reinstall the plugins you removed, and delete the copies only after a few days of normal use.
A migration that never finishes: the real state of issue #18066
The symptom. Issue jellyfin/jellyfin #18066, "Migration ChangeOwnerIdToGuid never completes and keeps one CPU core at 100%", was opened on 15 September 2026 on an upgrade from 10.11.11 to 12.1.0. The reporter describes a .NET thread at about 100% of one core for over two hours, a jellyfin.db database of about 3.2 to 3.5 GB and some 311,000 items, with almost no disk writes and no error in the log.
Second case: the restart by apt upgrade. Another user reports that an apt upgrade restarted the service mid-migration; every later start stayed stuck on the AddForeignKeyToOwnerId migration, at 0% CPU, with no I/O or writes. They also note that a local systemd file limiting the startup timeout (TimeoutSec = 15) was killing migrations. They restored the backup Jellyfin creates before migrating (the data/SQLiteBackups/ folder): 12.2 then replayed the migrations in about five minutes.
What worked, what did not. Some users succeeded after letting the migration run all night, others after restoring a pre-migration copy or reinstalling Jellyfin; some went back to 10.11.11 meanwhile. Blockages persist on 12.2 for at least two of them, while at least five report a successful upgrade there.
The upstream fix. Pull request #18073, "Fix ChangeOwnerIdToGuid migration performance", was merged on 26 September 2026 into the 12.z branch (milestone v12.2) and backported on 27 September. It addresses two slow spots: a collision check that scanned the whole UserData table for every placeholder row, and seven indexes built on a multi-gigabyte table then discarded right away when SQLite rebuilds the table to add the foreign key.
State on 7 November 2026. The issue is still open (bug label), with no root-cause analysis from a maintainer in the visible thread; the last comment dates from 17 October. Versions 12.0, 12.1, 12.2, 12.3 and 12.4 exist (12.4 of 26 October is the latest): aim for the latest 12.x rather than 12.0 or 12.1, published before this performance fix was merged.
Cautious reading: the fix shortens migration time, it does not guarantee that every database finishes, and nothing replaces the prior copy. A separate case in the thread, processes at 100% after a user logged in, was tied by its author to stale SQLite statistics (sqlite_stat1) and fixed with ANALYZE; its link to the migration is not confirmed.
Troubleshooting — common errors
Empty library after a scan: check that the Docker bind mount is right and that permissions match. The container runs as UID 1000; if your files belong to root, Jellyfin cannot read them: chown -R 1000:1000 /mnt/media.
HTTPS broken behind nginx: if the interface loads over HTTP although you configured HTTPS, check that the X-Forwarded-Proto and X-Real-IP headers are passed in the nginx configuration. Without them, Jellyfin generates HTTP URLs in its responses.
Slow transcoding or VAAPI error: confirm that the container can reach the device with docker exec jellyfin ls /dev/dri. If the folder is missing, the device is not passed in docker-compose.yml, or your VPS does not expose it.
Mobile apps refuse to connect: check that your TLS certificate is valid and not self-signed. The Jellyfin iOS and Android clients reject self-signed certificates without extra configuration. If you use Let's Encrypt, make sure automatic renewal (certbot renew) is active: systemctl status certbot.timer.
Plugins installed but inactive after a restart: some plugins need a FULL Jellyfin restart (not a configuration reload) to activate. Use docker compose restart rather than the restart option in the interface if the problem persists.
"Server is still starting up." in a loop after a version upgrade: a database migration is running or blocked. Do not restart. Look at the migration counter in docker compose logs, the load with docker stats, then apply the wait-or-restore decision described above.
An old client no longer connects after 12.0: the /emby/* and /mediabrowser/* routes were removed. Update the client or switch to another one.
Deploy Jellyfin from the ServOrbit Marketplace
If you prefer a preconfigured install over managing Docker by hand, Jellyfin is available in the ServOrbit Marketplace. The install automatically creates the container and, as soon as you attach a domain to the application from the client area, installs the nginx vhost and the TLS certificate. You get a ready-to-use Jellyfin, with the setup wizard on first access; media storage (additional volume, Storage Box) is still yours to mount, as described above.
The VPS stays entirely under your control: you can connect over SSH, edit docker-compose.yml, add volumes or move to another infrastructure at any time.
Mind one particularity of the template: on 7 November 2026 it references the jellyfin/jellyfin:latest image and mounts the jellyfin-data volume on /config. After deployment, pin the image tag yourself to the version you chose, then follow the upgrade procedure above before any change. This guide is provided without support for your installation.