Jellyfin Media Server - Docker Deployment and Remote Access
Status: Active
Last Updated: 2026-08-26
Category: Cloud - Media Services
Prerequisites: docker-compose-intro, traefik-v3-reverse-proxy
Time: 2-4 hours
Tags: jellyfin, media, docker, transcoding, vaapi, reverse-proxy
Summary
Deploying Jellyfin โ the open-source media server โ on fogserv.cloud with Docker Compose: library organization, hardware-accelerated transcoding via VAAPI, user management, and secure remote access through the Traefik reverse proxy. Includes guidance on which data to back up (config, not media) and how playback decisions drive server load.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Deploy Jellyfin with a production-grade compose file
- โ Organize libraries so metadata scraping works first try
- โ Enable VAAPI/QSV hardware transcoding and verify it is actually used
- โ Publish Jellyfin safely behind Traefik with TLS
- โ Decide what belongs in backups for this service
Table of Contents
- Why Jellyfin
- Docker Compose Deployment
- Libraries & File Layout
- Transcoding & Hardware Acceleration
- Remote Access via Reverse Proxy
- Users & Backups
Context / Why This Matters
Jellyfin is the media component of the homelab stack: it serves the same media library that other services reference, with no licensing or account requirements. Unlike Nextcloud or Immich, its data model is asymmetric โ a few MB of irreplaceable config/database against terabytes of re-downloadable media. That asymmetry drives every operational decision here: aggressive exclusion of media from backups (storage-backup-strategies), but rigorous backup of the config volume.
Implementation / Core Content
Why Jellyfin
Compared to Plex/Emby, Jellyfin is fully open source, needs no external account or "pass", and works offline. The trade-off is that you own more of the configuration โ which is exactly what this article covers.
Docker Compose Deployment
# /opt/jellyfin/docker-compose.yml
services:
jellyfin:
image: jellyfin/jellyfin:10.10
container_name: jellyfin
hostname: fogserver-jellyfin
restart: unless-stopped
environment:
- TZ=Europe/Berlin
- JELLYFIN_PublishedServerUrl=https://jellyfin.fogserv.cloud
devices:
- /dev/dri:/dev/dri # GPU for VAAPI/QSV transcoding (Intel/AMD iGPU)
volumes:
- ./config:/config # settings, DB, metadata โ BACK THIS UP
- ./cache:/cache # transcoding scratch, thumbnails cache
- type: bind
source: /srv/media
target: /media
ports:
- "8096:8096" # HTTP UI/API (LAN); TLS terminates at proxy
cd /opt/jellyfin && docker compose up -d
docker compose logs -f # wait for "Startup complete"
First-run wizard at http://server.lan:8096: create admin user, set language/metadata locales, add libraries.
Pin a specific minor version (10.10, not latest) and bump deliberately after backing up ./config โ Jellyfin migrates its database on version changes.
Libraries & File Layout
Scraping succeeds or fails based on directory naming before any settings matter:
/srv/media/
โโโ movies/
โ โโโ Blade Runner 2049 (2017)/
โ โโโ Blade Runner 2049 (2017).mkv
โ โโโ poster.jpg # optional local artwork
โโโ shows/
โ โโโ Breaking Bad (2008)/
โ โโโ Season 01/
โ โ โโโ Breaking Bad S01E01.mkv
โ โ โโโ Breaking Bad S01E02.mkv
โโโ music/
โโโ Artist/Album/01 - Track.flac
Rules that prevent 90% of metadata pain:
- One movie per folder; include year in parentheses for ambiguous titles
- Episode pattern
SxxExx; seasons asSeason 01folders - Never mix movies and shows in one library; create separate libraries per content type
- Set each library's Metadata downloaders order explicitly (e.g., TheMovieDb first)
Library options worth setting: enable Real Time Monitoring (inotify) on movie/show libraries so new files appear without manual scans; disable chapter image extraction on huge libraries (slow initial scan).
Transcoding & Hardware Acceleration
Direct play (client supports the file natively) costs almost nothing. Transcoding burns CPU/GPU. The goal: maximize direct play, offload unavoidable transcodes to hardware.
Enable in Dashboard โ Playback โ Transcoding: Hardware acceleration = VAAPI (Intel/AMD) or NVIDIA NVENC, select /dev/dri/renderD128, tick H.264/HEVC decode+encode formats.
Verify hardware transcoding actually engages:
- Start playback of a high-bitrate HEVC file in a browser (forces transcode)
- Watch
Dashboard โ Activity โ Active tasksโ task should show "(transcoding)" - On the host during transcode:
intel_gpu_top(orradeontop) should show Video engine activity while CPU stays low - Server logs show
VaapiEncoderentries rather thanlibx264
Sizing rules of thumb: an Intel iGPU handles 2โ3 simultaneous 1080p transcodes comfortably; software x264 1080p wants roughly one modern core per stream. If remote users see buffering with CPU idle, the problem is usually network or subtitle burning (image-based subtitles like PGS force transcode even when codecs match).
Tune clients toward direct play: prefer Jellyfin apps (Android TV, iOS, Kodi addon) over browsers; browsers can't handle many containers/codecs natively.
Remote Access via Reverse Proxy
Expose only the reverse proxy publicly; keep 8096 bound to LAN. With Traefik (traefik-v3-reverse-proxy):
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyfin.rule=Host(`jellyfin.fogserv.cloud`)"
- "traefik.http.routers.jellyfin.entrypoints=websecure"
- "traefik.http.routers.jellyfin.tls.certresolver=le"
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
Then in Jellyfin's Networking settings:
- Set Published server URL to the public HTTPS address (also set via env above)
- Add LAN CIDRs to Known proxies / Local network so LAN clients still count as local
- Leave "Allow remote connections" on; require auth always
Security checklist for public exposure:
- All accounts get strong passwords; disable "Allow password reset" via email unless configured
- Consider failing2ban-style protection at proxy level for brute-force (../security/fail2ban-setup.md)
- Alternative zero-exposure option: VPN access instead of publishing (../security/wireguard-vpn.md) โ best security, worse convenience for family users
TLS termination details and cert automation live in ../security/tls-configuration.md and ../security/letsencrypt-automation.md.
Users & Backups
User management (Dashboard โ Users):
- Create per-person accounts; use Access tags/library limits to restrict kids' profiles to appropriate libraries
- Enforce scheduled inactivity logout for shared devices
What to protect:
| Data | Location | Verdict |
|---|---|---|
| Config + embedded DB | /opt/jellyfin/config |
Back up (restic, daily) |
| Thumbnails/cache | ./config/data subpaths, /cache |
Exclude |
| Media files | /srv/media |
Exclude (re-downloadable) |
| Watch states | inside config DB | Covered by config backup |
restic backup /opt/jellyfin/config --tag jellyfin \
--exclude '/opt/jellyfin/config/data/**' \
--exclude '/opt/jellyfin/config/cache/**'
A full Jellyfin rebuild from backup is: redeploy compose file, restore config dir, remount media โ under 30 minutes (RTO).
Practical Examples
Example 1: Diagnose "playback buffers remotely"
docker exec jellyfin tail -f /config/log/log_*.log | grep -i -E 'transcode|direct'
intel_gpu_top # video engine busy? โ HW transcode working
Decision tree: logs say DirectPlay + still buffering โ bandwidth problem (check upload speed / MTU). Logs say transcoding with libx264 โ acceleration not enabled or /dev/dri missing in container.
Example 2: Add a new library without rescanning everything
Add files under correct naming scheme โ inotify picks up new items within seconds. Verify: Dashboard โ Scheduled tasks shows no full scan triggered; new title appears in library.
Example 3: Version upgrade procedure
cd /opt/jellyfin
docker compose down
tar czf ~/jellyfin-config-pre-upgrade.tgz config/
sed -i 's/jellyfin:10\.10/jellyfin:10.11/' docker-compose.yml
docker compose pull && docker compose up -d
docker compose logs -f # watch migration messages
Rollback = restore previous tag + unpack the tarball.
Troubleshooting & Common Pitfalls
| Problem | Cause | Fix |
|---|---|---|
| No hardware encode options in dashboard | /dev/dri not passed through; drivers missing on host |
Add device mapping; install intel-media-va-driver on host; ls /dev/dri must exist |
| Transcode falls back to libx264 despite VAAPI | Codec not ticked in acceleration formats, or driver lacks HEVC encode | Tick formats; check vainfo output on host |
| Metadata wrong/duplicated titles | Bad folder naming; mixed library types | Rename per layout rules; separate libraries per type |
| Remote playback fails only outside LAN | Published URL mismatch; proxy missing websocket support | Set PublishedServerUrl; verify Traefik routes websockets (default OK in v3) |
| Subtitles cause constant transcoding | Image-based subs (PGS/VOBSub) burned into video | Prefer SRT text subtitles in library settings |
| Library scan pegs disk for hours | Chapter image extraction on huge library | Disable chapter images; raise scan interval |
| Watch states lost after rebuild | Config volume excluded from backup too broadly | Back up config minus cache paths only |
Next Steps / Ops Actions
- Add
/opt/jellyfin/configto nightly restic job: backup-to-object-storage - Schedule the job with alerting: backup-automation
- Confirm TLS grade of the public endpoint: tls-configuration
- Record achieved remote-transcode capacity (streams @ resolution) in your ops notes
Sources & Related Articles
External references consulted:
- https://jellyfin.org/docs/general/installation/container/
- https://jellyfin.org/docs/general/administration/hardware-acceleration/
- https://jellyfin.org/docs/general/networking/
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation by KB writing session.