Self-Hosting Jellyfin: Complete Media Server Setup Guide
Set up Jellyfin on Docker with hardware transcoding, organize your library, and access it from any device. Full walkthrough with working Docker Compose config.
This post contains affiliate links. If you buy through them, I earn a small commission at no extra cost to you.
Jellyfin is a completely free, open-source media server. No Plex Pass. No subscriptions. No telemetry. You point it at your media files and it serves them to any device — phone, TV, browser, desktop app — with a polished interface, automatic metadata, and hardware-accelerated transcoding.
If you have movie or TV files sitting on a hard drive, Jellyfin is the fastest path from “files on disk” to “streaming service on your couch.”
This guide uses Docker Compose. If you haven’t set up Docker yet, start with the Getting Started guide and Docker Compose basics. If you’re still deciding how to run Jellyfin — dedicated VM, LXC, or bare metal — the Proxmox VM vs LXC guide covers that decision. For remote access after setup, add Tailscale.
Prerequisites
- Docker and Docker Compose installed on your server
- Media files stored on your server (or accessible via network share)
- 10—15 minutes
Step 1: Create the directory structure
mkdir -p /opt/jellyfin/{config,cache}
mkdir -p /data/media/{movies,tvshows,music}
What these are:
/opt/jellyfin/config— Jellyfin’s configuration, database, and metadata cache. Back this up./opt/jellyfin/cache— Transcoding cache and thumbnails. Expendable — can be regenerated./data/media/movies,/data/media/tvshows,/data/media/music— your media
Adjust the media paths to wherever your actual files live. If you’re still figuring out where to store media, decide on a location before going further — a dedicated drive or directory under /data/media is the standard pattern.
Step 2: Write the Docker Compose file
Create /opt/jellyfin/docker-compose.yml:
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
user: "1000:1000"
network_mode: host
volumes:
- /opt/jellyfin/config:/config
- /opt/jellyfin/cache:/cache
- /data/media:/media:ro
environment:
- JELLYFIN_PublishedServerUrl=http://YOUR_SERVER_IP:8096
restart: unless-stopped
A few notes on these config choices:
network_mode: host — Jellyfin uses service discovery features (DLNA, local network detection) that work better with host networking. This means Jellyfin uses your server’s IP directly rather than a Docker bridge network.
user: "1000:1000" — Runs Jellyfin as your regular user rather than root. Replace 1000:1000 with your actual user and group IDs (run id in the terminal to check). Your media directory needs to be readable by this user.
/media:ro — The :ro flag mounts your media read-only. Jellyfin doesn’t need to write to your media directories.
JELLYFIN_PublishedServerUrl — Replace YOUR_SERVER_IP with the actual IP of your server on your local network.
Step 3: Start Jellyfin
cd /opt/jellyfin
docker compose up -d
Check the logs to confirm it started cleanly:
docker compose logs -f jellyfin
You should see lines like [INF] Startup complete within 30 seconds. Hit Ctrl+C to exit the log follow.
Step 4: Initial setup wizard
Open a browser and go to http://YOUR_SERVER_IP:8096. The Jellyfin setup wizard will walk you through:
- Creating an admin account — use a strong password
- Adding media libraries (point each one at the relevant subdirectory under
/media) - Setting up metadata providers
Jellyfin will immediately start scanning and fetching metadata. For a large library, the initial scan can take 15—30 minutes. You can start watching while it runs.
Step 5: Enable hardware transcoding
Hardware transcoding lets Jellyfin offload video encoding to your GPU rather than using the CPU. Without it, a single 4K transcode can pin your CPU. With it, the same transcode barely registers.
First, check if the DRI device exists on your server:
ls /dev/dri/
You should see renderD128. If it’s there, add this to your compose file under the jellyfin service:
devices:
- /dev/dri/renderD128:/dev/dri/renderD128
group_add:
- "render"
- "video"
Then in Jellyfin’s admin panel, go to Dashboard > Playback > Transcoding and select your hardware type:
Intel Quick Sync (N-series, 12th/13th gen+)
Set Hardware acceleration to Intel QuickSync (QSV) and enable the codecs you want offloaded (H.264, HEVC, AV1 if supported).
AMD (Radeon integrated graphics)
AMD also uses /dev/dri/renderD128. Add the same devices and group_add blocks. In transcoding settings, select Video Acceleration API (VAAPI). The Radeon 680M and 780M in recent AMD mini PCs handle 4K transcoding well.
NVIDIA (dedicated GPU)
Install the NVIDIA Container Toolkit on the host first. Then add to your compose file:
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]
In transcoding settings, select NVIDIA NVENC.
Verify it’s working
Start a stream that requires transcoding, then check Dashboard > Active Sessions. A hardware-accelerated transcode shows a small GPU utilization bump with CPU staying low. A software transcode shows significant CPU utilization.
Step 6: Set up remote access
For access outside your home, you have two good options:
Option A: Tailscale (recommended)
Install Tailscale on your server and all the devices you want to use for streaming. Once connected to your tailnet, access Jellyfin at http://100.x.x.x:8096 (your server’s Tailscale IP) from anywhere. No port forwarding, no SSL config, just works.
See the Tailscale setup guide for the 10-minute setup. Update JELLYFIN_PublishedServerUrl in your compose file to use your Tailscale IP so the app always gets a reachable address regardless of which network you’re on. Tailscale traffic is encrypted end-to-end — no HTTPS termination needed for Tailscale use.
Option B: Nginx Proxy Manager + domain
If you want a URL like https://jellyfin.yourdomain.com, set up NPM and add a proxy host pointing to jellyfin:8096. See the NPM setup guide for SSL and reverse proxy configuration.
Step 7: Install Jellyfin apps
Jellyfin has native apps for:
- Android / iOS — “Jellyfin” on Play Store / App Store
- Apple TV / Fire TV / Roku — Available in their app stores
- Android TV / Google TV — Play Store
- Smart TVs (Samsung, LG) — Use the built-in browser or a Chromecast / Fire Stick
For the best experience on a TV, use the native app for your platform rather than the browser. Native apps support hardware decoding and have better buffering behavior.
Organizing your media
Jellyfin uses filenames to identify media and match against metadata providers (The Movie Database, TheTVDB). The more closely your filenames match the expected format, the better the matching works.
Movies:
/media/movies/The Matrix (1999)/The Matrix (1999).mkv
/media/movies/Inception (2010)/Inception (2010).mkv
TV Shows:
/media/tvshows/Breaking Bad/Season 01/Breaking Bad S01E01.mkv
/media/tvshows/Breaking Bad/Season 01/Breaking Bad S01E02.mkv
The year in parentheses helps Jellyfin distinguish between films with the same name. The S01E01 format is the most reliably detected episode naming scheme. If you have a large existing library in a different format, add a small test folder first, verify Jellyfin identifies it correctly, then move files in batches. FileBot is the standard tool for bulk renaming.
Backups
Back up /opt/jellyfin/config. That directory contains your user accounts, library configuration, watched status, and metadata cache. If you lose it, you’re rebuilding from scratch.
A simple approach: add a cron job that tarballs the config directory daily and copies it off the server:
0 3 * * * tar -czf /backup/jellyfin-config-$(date +\%Y\%m\%d).tar.gz /opt/jellyfin/config
Keep a week of daily backups. Jellyfin configs are small — a week of backups is typically a few hundred megabytes at most. The homelab backup guide covers the broader backup strategy.
Updating Jellyfin
cd /opt/jellyfin
docker compose pull
docker compose up -d
Jellyfin releases frequently. The latest tag makes updates trivial. For automated container updates, Watchtower can handle this for you.
Useful settings to configure after setup
Networking > Enable automatic port mapping — Turn this off. You don’t need UPnP opening ports on your router.
Libraries > Refresh metadata — If Jellyfin pulled wrong metadata for a movie (common with obscure titles), right-click the item > “Identify” and search manually.
Users > Access schedules — Useful if you want to restrict certain users (kids) to specific hours or libraries.
Dashboard > Scheduled Tasks — Jellyfin runs metadata refresh, library scan, and cleanup tasks on a schedule. Check these and adjust if your server is doing too much at inconvenient times.
Troubleshooting common issues
“Playback error” or constant buffering
Usually transcoding. Check Dashboard > Active Sessions. If it says “Transcode” without “(hw)”, your hardware transcoding isn’t enabled or isn’t working. Fall back to software transcoding temporarily by setting the transcoding setting to “None (Software)” — if playback works, the issue is hardware transcoding config.
Metadata is wrong
Right-click the item > “Identify”. Search by IMDB ID if the title is ambiguous. For TV shows, make sure your folder structure matches the expected format (Season XX).
Container won’t start
Check docker compose logs jellyfin. The most common cause is a permissions problem — the Jellyfin container runs as a specific UID and needs read access to your media directories. If your media is owned by root, either chmod o+r the directory or run Jellyfin with user: "root" in the Compose file (less ideal but functional).
Once Jellyfin is running, you have a media server that’s genuinely better than Plex for most use cases: faster scanning, no subscription required for sync and downloads, and full control over your data.
For monitoring server health during transcoding-heavy usage, Grafana + Prometheus gives you the CPU/GPU dashboards worth having. Immich, Vaultwarden, Paperless-ngx, and Uptime Kuma are the other high-value services worth adding next.