Files
docudjeex/content/en/3.serveex/5.media/1.jellyfin.md
T

13 KiB

title, description
title description
Jellyfin Install Jellyfin, a free and open-source media server, to stream your movies and TV shows from anywhere without a paid subscription.

:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}

Jellyfin

Jellyfin is a free, open-source alternative to Plex and Emby. Unlike Plex, it has no paid tiers, no telemetry, and doesn't require an online account to use or manage your server: everything runs locally and stays yours.

It covers the same basics: a media library with metadata and artwork, transcoding (including hardware acceleration), and apps for TV, Android, iOS, Windows, and macOS.

As always, we'll use the linuxserver.io image.

::note{to="/serveex/core/wireguard"}

Unlike Plex, Jellyfin has no cloud relay: to access your server outside your local network, you must expose it yourself (see below), or use a VPN like Wireguard. ::

Install Jellyfin

::file-tree

tree: /: - docker: - jellyfin: - compose.yaml - .env - config/ - media: - tvseries/ - movies/ - library/

::

::steps{level="3"}

Create the media folders

Create the movies, tvseries, and library folders in /media:

mkdir -p /media/movies /media/library /media/tvseries

Deploy the stack

Open Dockge, click compose, name the stack jellyfin, and add the following config:

---
services:
  jellyfin:
    image: lscr.io/linuxserver/jellyfin:latest
    container_name: jellyfin
    environment:
      - PUID=${PUID}
      - PGID=${GUID}
      - TZ=Europe/Paris
    volumes:
      - /docker/jellyfin/config:/config
      - /media:/media
    restart: unless-stopped
    devices:
      - /dev/dri:/dev/dri
    ports:
      - 8096:8096

::tip{icon=""} Add the Watchtower label to automate updates:

---
services:
  jellyfin:
    #...
    labels:
      - com.centurylinklabs.watchtower.enable=true

::

Set your environment variables

Find your PUID and GUID by running:

id username

Fill in your .env file with the retrieved values, for example:

PUID=1000
GUID=1000

Deploy the stack. The local interface is available at http://yourserverip:8096.

::note{to="https://docs.linuxserver.io/images/docker-jellyfin/#hardware-acceleration"}

The /dev/dri device is only needed for hardware-accelerated transcoding on Intel/AMD GPUs. Remove it if your server doesn't have one, or adapt it for an NVIDIA GPU following linuxserver.io's documentation. ::

Done !

::

Configure Jellyfin

On first visit, Jellyfin walks you through a setup wizard:

  • Choose a display language and create your admin account.
  • Add a media library, pointing to /media/movies for movies and /media/tvseries for TV shows.

And that's it! Add your media to /media/movies and /media/tvseries on your server. See below to start watching, at home or remotely.

::note{to="/general/networking/samba"}

If your media is stored on a network disk (e.g. NAS or external hard drive over the network), refer to the Samba mount guide so Jellyfin can access it. ::

Hardware Transcoding

Jellyfin re-encodes video on the fly whenever a client can't play a file as-is: the device's screen resolution is smaller than the source, its network connection is too slow for the file's bitrate, or it doesn't support the file's codec, HDR format, or subtitle type. This is called transcoding. Done purely in software, on the CPU, it's slow and can max out a modest server with just one or two simultaneous streams. Hardware transcoding offloads this work to your CPU's built-in GPU (Intel QuickSync, on most homelab hardware), which handles it far faster and lets the CPU stay free for everything else.

Tone mapping is a related, separate feature: converting HDR video (which needs a compatible HDR display to look right) down to SDR so it displays correctly on a screen, TV, or client that doesn't support HDR, instead of looking washed out or overly dark.

::note{to="https://docs.linuxserver.io/images/docker-jellyfin/#hardware-acceleration"} This only applies to Intel (and some AMD) integrated GPUs passed through with /dev/dri, already included in this guide's compose.yaml. NVIDIA GPUs need a different setup entirely, covered in linuxserver.io's documentation. If your server has no compatible GPU, transcoding still works, just entirely on the CPU. ::

::steps{level="3"}

Add the OpenCL mod

Tone mapping specifically needs an Intel OpenCL runtime, which this image doesn't ship with by default. Add the dedicated mod to your compose.yaml:

services:
  jellyfin:
    environment:
      - DOCKER_MODS=linuxserver/mods:jellyfin-opencl-intel

::note Don't use the generic universal-package-install mod with intel-opencl-icd for this: it installs an older runtime from Ubuntu's own repositories that's a known source of tone mapping bugs. This dedicated mod ships a newer, purpose-built one. ::

Redeploy the stack for the mod to take effect.

Enable hardware acceleration

In Jellyfin, go to Dashboard > Playback and set Hardware acceleration to Intel QuickSync (QSV) (or VA-API, if QSV isn't available for your CPU generation).

Then uncheck any codec your specific GPU doesn't support hardware-decode for. Intel's own hardware acceleration page lists exactly which codecs each CPU generation supports.

Enable tone mapping

Further down the same page, enable Tone mapping. This only takes effect once the OpenCL mod above is installed and the stack redeployed; enabling it without that step first will make HDR transcodes fail instead of just skipping the tone mapping.

Done !

::

Watching at Home

While connected to your home network (Wi-Fi or Ethernet, on the same LAN as your server), no exposure or VPN is needed at all. Use Moonfin wherever it's available (phones, tablets, Android TV/Google TV, desktop, web) instead of the official Jellyfin app: it looks and works the same everywhere, and can sync your settings across devices via its own server plugin. Connect to http://yourserverlanip:8096.

Samsung Tizen and LG webOS get a separate, dedicated Moonfin app instead (same name, different codebase), and Apple TV is best served by Neptune, a dedicated tvOS client with native HEVC/AV1/VP9 decoding and full Atmos passthrough. Where none of these are available (Fire TV, Roku, Kodi), see the table below:

Platform Recommended app
Phone / Tablet (Android, iOS) Moonfin (Play Store / App Store)
Android TV / Google TV Moonfin (Play Store or APK)
Apple TV Neptune (App Store)
Samsung Tizen / LG webOS Moonfin (sideloaded); the official apps exist but lag behind
Fire TV Official Jellyfin app (Amazon Appstore)
Roku Official Jellyfin app
Kodi Jellyfin for Kodi (syncs your library into Kodi), or JellyCon for lighter streaming-only boxes

::caution

If it fails: check your firewall rules. ::

Watching Away From Home

::warning Jellyfin won't be exposed to the internet on this site, unlike most other apps. Its native apps (phone, TV, Chromecast...) talk directly to its API rather than a browser, so a forward-auth layer like TinyAuth or Authentik can't sit in front of it without breaking login and playback on those clients, and Jellyfin itself has no built-in 2FA or OIDC to fall back on. So instead of exposing it, we'll reach it through the WireGuard VPN already set up earlier in this guide. ::

Instead of routing a whole device's traffic through the VPN, or even the whole 10.8.0.0/24 VPN subnet, we'll use a tighter split tunnel: each remote device only routes traffic for your server's own VPN address through the tunnel. Everything else (browsing, other apps) stays on the device's normal connection, and the VPN is only ever used to reach that one machine.

::note{to="/serveex/core/wireguard#client-server-setup"}

This assumes your Jellyfin server already runs its own WireGuard client, connecting it to your VPN as described in Client Server Setup. Note the VPN address wg-easy assigned it (e.g. 10.8.0.2); that's the address every device below will target. ::

::caution{to="#restricting-a-third-party-peer-to-just-jellyfin"} This split tunnel is a client-side convention, not a server-enforced security boundary: AllowedIPs is a routing setting, and wg-easy doesn't add any firewall rule to back it up by default. Anyone with access to a device (or its .conf file) could edit AllowedIPs back to something broader and reach whatever else your WireGuard server can route to, including other connected peers. If a peer is one of your own devices, that's usually an acceptable risk. If you're handing a peer to a third party (a friend, family member) who should only ever reach Jellyfin, see Restricting a third-party peer to just Jellyfin below for a way to actually enforce that server-side. ::

::steps{level="3"}

Create a client in wg-easy for each device

Following Retrieve Configuration Files, create one client per device (phone, computer, TV/streaming box) and download its config file.

Narrow the tunnel to just the server

Open each downloaded .conf file and replace its AllowedIPs line so it only covers your Jellyfin server's VPN address, as a /32 (a single IP, not a range):

AllowedIPs = 10.8.0.2/32

::note

Replace 10.8.0.2 with whatever address your server actually got. Leaving the default 10.8.0.0/24 here would tunnel traffic for every VPN peer, not just your server. ::

Import the config into each device

  • Phone: install the WireGuard app, then import the edited .conf file (or scan a QR code generated from it).
  • Computer: install the WireGuard client and import the file.
  • TV: if it runs Android TV or Google TV, install WireGuard from the Play Store. Copy the .conf file onto a USB drive, plug it into the TV or box, then use the app's Add from file option to browse the USB drive and import it. Most other TV operating systems (Tizen, webOS...) can't install a VPN client directly, so connect through a device that already has the tunnel active instead (an Android TV box, or casting from a phone or computer that's connected).

::warning A .conf file is a credential, not just a settings file: whoever has it can connect to your VPN as that device. Once it's imported, delete it from the USB drive (and anywhere else you copied it, like Downloads folders), and never share it with anyone else. ::

Done !

::

Restricting a third-party peer to just Jellyfin

wg-easy (v15.3+) can enforce this itself, with a real firewall rule inside its own container rather than a client-side setting. This is the right approach when the peer belongs to someone else, not one of your own devices.

::steps{level="4"}

Enable the per-client firewall

In wg-easy's admin panel, go to the interface settings and enable Per-Client Firewall. It's marked experimental, but works by generating a dedicated iptables chain inside the wg-easy container.

Restrict the peer's allowed destination

Edit the third party's client and set its Firewall Allowed IPs to your Jellyfin server's VPN address, e.g. 10.8.0.2, or narrower still, 10.8.0.2:8096/tcp to also restrict the port and protocol.

::note

Leaving this field empty falls back to the client's own AllowedIPs, which isn't restrictive: it must be set explicitly for this peer. This also cuts off that peer's regular internet access through the tunnel, since only traffic to Jellyfin is now allowed, which is the intended effect for a guest peer. ::

Verify the rule was applied

docker exec wg-easy iptables -L WG_CLIENTS -n -v

You should see an ACCEPT line for that peer's IP pointing only at your Jellyfin server, followed by a catch-all DROP.

Done !

::

Once connected, open the Jellyfin app or http://10.8.0.2:8096 (your server's VPN address) from that device. Only traffic to that address goes through the VPN; the rest of the device's connection is untouched.