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 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
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
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/moviesfor movies and/media/tvseriesfor 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
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
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
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
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
.conffile (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
.conffile 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.
