Self-Hosted Media Server: Jellyfin, Plex and Emby with Docker
Streaming services have fragmented into a dozen competing platforms, each with their own subscription fee and content library. A self-hosted media server puts you back in control: your media, organized your way, accessible from any device, with no monthly fees and no content disappearing due to licensing changes. The three leading options are Jellyfin (fully open source), Plex (freemium with optional paid tier), and Emby (freemium with paid premium).
Jellyfin vs Plex vs Emby
| Feature | Jellyfin | Plex | Emby |
|---|---|---|---|
| License | GPL-2.0 (fully open source) | Proprietary (freemium) | Proprietary (freemium) |
| Cost | Free, no paid tiers | Free + Plex Pass ($5/mo or $120 lifetime) | Free + Emby Premiere ($4.99/mo or $119 lifetime) |
| Account required | No (local auth) | Yes (Plex account mandatory) | No (local auth) |
| Hardware transcoding | Free (VAAPI, NVENC, QSV) | Plex Pass required | Premiere required |
| Remote access | Self-managed (reverse proxy) | Built-in relay service | Self-managed |
| Live TV / DVR | Yes (free) | Yes (Plex Pass) | Yes (Premiere) |
| Mobile apps | Free (official + third-party) | Free with ads / paid unlock | Paid unlock required |
| Plugins/Extensions | Yes (community plugins) | Limited | Yes (plugin system) |
| Client ecosystem | Good (web, apps, Kodi, Infuse) | Excellent (widest platform support) | Good (web, apps) |
| Metadata providers | TMDb, OMDb, local NFO files | Plex agents + custom | TMDb, OMDb, TVDb |
For self-hosters who value open source and data sovereignty, Jellyfin is the clear choice. Plex is superior if you need the widest client compatibility and are willing to pay for Plex Pass. Emby occupies a middle ground but has a smaller community than either alternative.
Docker Setup: Jellyfin
# docker-compose.yml for Jellyfin
version: "3.8"
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
restart: unless-stopped
user: "1000:1000"
environment:
- JELLYFIN_PublishedServerUrl=https://media.example.com
volumes:
- jellyfin_config:/config
- jellyfin_cache:/cache
- /mnt/media/movies:/data/movies:ro
- /mnt/media/tv:/data/tv:ro
- /mnt/media/music:/data/music:ro
ports:
- "8096:8096"
# DLNA (optional)
# - "1900:1900/udp"
# - "7359:7359/udp"
# Hardware transcoding - Intel QSV/VAAPI
devices:
- /dev/dri:/dev/dri
# Hardware transcoding - NVIDIA
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: 1
# capabilities: [gpu]
volumes:
jellyfin_config:
jellyfin_cache:
Docker Setup: Plex
# docker-compose.yml for Plex
version: "3.8"
services:
plex:
image: lscr.io/linuxserver/plex:latest
container_name: plex
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
- VERSION=docker
- PLEX_CLAIM=${PLEX_CLAIM_TOKEN} # Get from plex.tv/claim
volumes:
- plex_config:/config
- /mnt/media/movies:/movies:ro
- /mnt/media/tv:/tv:ro
- /mnt/media/music:/music:ro
# Transcode directory (use SSD or tmpfs for performance)
- /tmp/plex_transcode:/transcode
ports:
- "32400:32400"
devices:
- /dev/dri:/dev/dri # Intel VAAPI/QSV
volumes:
plex_config:
Docker Setup: Emby
# docker-compose.yml for Emby
version: "3.8"
services:
emby:
image: emby/embyserver:latest
container_name: emby
restart: unless-stopped
environment:
- UID=1000
- GID=1000
- GIDLIST=44,105 # video and render groups
volumes:
- emby_config:/config
- /mnt/media/movies:/mnt/movies:ro
- /mnt/media/tv:/mnt/tv:ro
ports:
- "8096:8096"
- "8920:8920" # HTTPS
devices:
- /dev/dri:/dev/dri
volumes:
emby_config:
Hardware Transcoding
Transcoding converts media from one format to another on the fly, which is essential when a client device does not support the original codec. Without hardware acceleration, transcoding is CPU-intensive and limits the number of simultaneous streams. Hardware transcoding offloads this work to the GPU.
Intel Quick Sync / VAAPI
Intel integrated graphics (6th generation and newer) provide excellent transcoding performance with minimal power usage:
# Verify Intel GPU is available
ls -la /dev/dri/
# Should show: card0, renderD128
# Check GPU capabilities
vainfo
# Should list H.264, HEVC decode/encode profiles
# Docker device mapping
devices:
- /dev/dri:/dev/dri
# Ensure the container user has access to the video group
# For Jellyfin with user: "1000:1000", add supplemental groups:
group_add:
- "44" # video group
- "105" # render group (varies by distro)
NVIDIA NVENC
NVIDIA GPUs provide powerful transcoding but require additional setup:
# 1. Install NVIDIA Container Toolkit
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
# 2. Verify GPU is accessible in Docker
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi
# 3. Docker Compose configuration
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
Transcoding Performance Comparison
| Method | Simultaneous 1080p Streams | Power Usage | Quality |
|---|---|---|---|
| Software (x264) | 1-2 (depends on CPU) | High | Excellent |
| Intel QSV (i5/i7) | 5-10+ | Low (~15W) | Good |
| NVIDIA NVENC (GTX 1660+) | 8-15+ | Medium (~30-75W) | Very good |
| AMD VCN (RX 5000+) | 3-8 | Low-Medium | Good |
Library Organization
Proper directory structure is critical for automatic metadata matching:
# Movies - one folder per movie
/mnt/media/movies/
The Matrix (1999)/
The Matrix (1999).mkv
The Matrix (1999).srt
Inception (2010)/
Inception (2010).mkv
# TV Shows - show > season > episodes
/mnt/media/tv/
Breaking Bad/
Season 01/
Breaking Bad - S01E01 - Pilot.mkv
Breaking Bad - S01E02 - Cat's in the Bag.mkv
Season 02/
Breaking Bad - S02E01 - Seven Thirty-Seven.mkv
# Music - artist > album > tracks
/mnt/media/music/
Pink Floyd/
The Dark Side of the Moon (1973)/
01 - Speak to Me.flac
02 - Breathe.flac
:ro) in Docker. This prevents the media server from accidentally modifying or deleting your files. Write access is only needed for metadata caching, which should go to a separate config/cache volume.
Remote Access with Nginx Reverse Proxy
# nginx/conf.d/jellyfin.conf
server {
listen 443 ssl http2;
server_name media.example.com;
ssl_certificate /etc/letsencrypt/live/media.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/media.example.com/privkey.pem;
# Large file uploads (for syncing)
client_max_body_size 20M;
# WebSocket support
location /socket {
proxy_pass http://jellyfin:8096;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
proxy_pass http://jellyfin:8096;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Disable buffering for streaming
proxy_buffering off;
}
}
Subtitle Management
Subtitles are essential for accessibility and multilingual media. Configure automatic subtitle downloading:
Jellyfin Plugin: Open Subtitles
- Install the "Open Subtitles" plugin from the Jellyfin plugin catalog
- Configure your OpenSubtitles.org API key
- Set preferred subtitle languages in library settings
- Enable automatic subtitle downloading for new media
Bazarr (Standalone Subtitle Manager)
# Add Bazarr to your media stack
bazarr:
image: lscr.io/linuxserver/bazarr:latest
container_name: bazarr
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York
volumes:
- bazarr_config:/config
- /mnt/media/movies:/movies
- /mnt/media/tv:/tv
ports:
- "6767:6767"
Bazarr integrates with Sonarr and Radarr to automatically download subtitles for your entire library. It supports multiple subtitle providers and can synchronize subtitle timing automatically.
Mobile and TV Clients
| Platform | Jellyfin | Plex | Emby |
|---|---|---|---|
| iOS | Swiftfin (free), Infuse | Plex (free with ads) | Emby (paid unlock) |
| Android | Jellyfin (free), Findroid | Plex (free with ads) | Emby (paid unlock) |
| Android TV | Jellyfin (free) | Plex (free) | Emby (paid unlock) |
| Apple TV | Swiftfin (free), Infuse | Plex (free) | Emby (paid unlock) |
| Roku | Jellyfin (free) | Plex (free) | Emby (paid unlock) |
| Web browser | Built-in | Built-in | Built-in |
| Kodi | JellyCon / Jellyfin for Kodi | Plex for Kodi | Emby for Kodi |
Performance Tuning
# Use tmpfs for transcode directory (uses RAM, much faster)
services:
jellyfin:
tmpfs:
- /config/transcodes:size=4G
# Or use an SSD-backed path
volumes:
- /ssd/jellyfin_transcode:/config/transcodes
# Network mode host for DLNA discovery (optional)
network_mode: host
For managing multiple media-related containers (Jellyfin, Bazarr, and related services), container management platforms like usulnet provide a unified view of resource usage across your media stack, making it easy to identify when transcoding is consuming excessive resources or when disk space is running low.
Summary
A self-hosted media server is one of the most satisfying self-hosting projects. Jellyfin provides a fully open-source solution with no paywalls or account requirements. Plex offers the most polished client experience and widest device support. Emby sits between the two. Regardless of which you choose, hardware transcoding is essential for a smooth multi-user experience, and proper library organization is the foundation of a well-functioning media server.