Running a media server at home means exposing your network to the internet, dealing with dynamic IP addresses, and competing with your household's bandwidth every time you want to stream remotely. Plex has become the default choice for most people, but it increasingly pushes users toward paid subscriptions, injects its own content into your library, and routes traffic through its servers even when it shouldn't need to. Jellyfin is a completely free, open-source alternative with no paid tiers, no telemetry, no accounts, and no central server dependency.
Running Jellyfin on a VPS solves the home network problem entirely. Your media lives in a datacenter with dedicated bandwidth, accessible from anywhere in the world without port forwarding, dynamic DNS, or NAT traversal. You get a streaming service that's entirely yours — no subscriptions, no content injection, no tracking, and no arbitrary limitations on features.
MassiveGRID Ubuntu VPS includes: Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10
Deploy a self-managed VPS — from $1.99/mo
Need dedicated resources? — from $19.80/mo
Want fully managed hosting? — we handle everything
Sizing Your VPS for Jellyfin
Jellyfin's resource requirements depend primarily on two factors: the size of your media library and whether you need real-time transcoding. If your clients can direct-play your media files (no format conversion needed), the CPU requirements are minimal. Transcoding — converting video on the fly to a format or resolution the client can handle — is extremely CPU-intensive.
| Use Case | Recommended VPS | Storage |
|---|---|---|
| Small library (up to 500GB), 1-2 streams, direct play | 2 vCPU / 2 GB RAM | 500 GB+ |
| Medium library (1-2TB), 2-3 streams, occasional transcoding | 4 vCPU / 4 GB RAM | 2 TB+ |
| Large library (5TB+), 3+ concurrent streams, frequent transcoding | Dedicated VPS 8+ vCPU / 16 GB RAM | 5 TB+ |
Sizing tip: For a small media library (up to 500GB) with 1-2 concurrent streams, deploy a Cloud VPS with 4 vCPU / 4GB RAM. Start with direct play and only invest in transcoding capacity if your clients need it.
Prerequisites
Before starting, ensure you have:
- An Ubuntu VPS provisioned and accessible via SSH — see our complete beginners guide if this is your first VPS
- Docker and Docker Compose installed — follow our Docker installation guide
- A domain name (e.g.,
media.yourdomain.com) with DNS pointed to your VPS - Sufficient storage for your media library
Check your available disk space:
df -h /
If you need more storage, MassiveGRID allows independent scaling of CPU, RAM, and storage — you can add storage without changing your compute plan.
Installing Jellyfin with Docker
Create the directory structure for Jellyfin:
sudo mkdir -p /opt/jellyfin
sudo mkdir -p /media/library/movies
sudo mkdir -p /media/library/shows
sudo mkdir -p /media/library/music
cd /opt/jellyfin
Create the Docker Compose file:
sudo nano /opt/jellyfin/docker-compose.yml
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
restart: unless-stopped
ports:
- "127.0.0.1:8096:8096"
volumes:
- jellyfin-config:/config
- jellyfin-cache:/cache
- /media/library:/media:ro
environment:
- JELLYFIN_PublishedServerUrl=https://media.yourdomain.com
# Uncomment for hardware transcoding (if available):
# devices:
# - /dev/dri:/dev/dri
# group_add:
# - "109" # render group ID (check with: getent group render)
volumes:
jellyfin-config:
jellyfin-cache:
Key configuration notes:
- We bind port 8096 to
127.0.0.1only — Jellyfin will be accessed through Nginx, not directly - The media directory is mounted as read-only (
:ro) inside the container so Jellyfin can't accidentally modify your files - Config and cache use named Docker volumes for persistence
- Hardware transcoding device passthrough is commented out — we'll cover that later
Start Jellyfin:
cd /opt/jellyfin
sudo docker compose up -d
Verify it's running:
sudo docker compose ps
curl -s http://127.0.0.1:8096/health
Initial Configuration and Library Setup
Before setting up Nginx, let's complete the initial configuration. Temporarily open port 8096 for setup, or use an SSH tunnel:
# SSH tunnel from your local machine:
ssh -L 8096:127.0.0.1:8096 user@your-vps-ip
Then open http://localhost:8096 in your browser. The setup wizard walks you through:
- Language: Choose your preferred display language
- User account: Create the admin account with a strong password
- Media libraries: Add your media directories
- Metadata: Configure metadata providers (TMDb for movies/TV, MusicBrainz for music)
- Remote access: Leave the defaults for now since we'll use Nginx
When adding media libraries, configure each type:
Movies Library
- Content type: Movies
- Folder:
/media/movies - Enable metadata downloaders: TMDb
- Enable image fetchers: TMDb
TV Shows Library
- Content type: Shows
- Folder:
/media/shows - Enable metadata downloaders: TMDb, TheTVDB
Music Library
- Content type: Music
- Folder:
/media/music - Enable metadata downloaders: MusicBrainz
Jellyfin expects your files to be named following a specific convention for automatic metadata matching:
# Movies
/media/library/movies/Movie Name (2024)/Movie Name (2024).mkv
# TV Shows
/media/library/shows/Show Name (2024)/Season 01/Show Name S01E01.mkv
/media/library/shows/Show Name (2024)/Season 01/Show Name S01E02.mkv
# Music
/media/library/music/Artist Name/Album Name (2024)/01 - Track Name.flac
Uploading Media to Your VPS
Getting your media files onto the VPS is often the most time-consuming part of the setup. Here are the most practical methods.
rsync (Best for Large Transfers)
rsync is the best tool for transferring large media libraries because it can resume interrupted transfers and only sends differences on subsequent syncs:
# Initial upload — from your local machine
rsync -avhP --partial /path/to/local/movies/ user@your-vps-ip:/media/library/movies/
# Subsequent syncs — only transfers new/changed files
rsync -avhP --partial /path/to/local/movies/ user@your-vps-ip:/media/library/movies/
The flags explained:
-a— archive mode (preserves permissions, timestamps, etc.)-v— verbose output-h— human-readable file sizes-P— show progress and enable partial transfers (resumable)--partial— keep partially transferred files for resume
For very large uploads over unreliable connections, use screen or tmux on the VPS to keep the transfer running even if your SSH session drops:
# On the VPS
sudo apt install screen
screen -S upload
# Start the rsync from within the screen session
rsync -avhP --partial user@source-machine:/path/to/media/ /media/library/
# Detach from screen: Ctrl+A then D
# Reattach later: screen -r upload
SFTP (For Selective Uploads)
For uploading individual files or small batches, SFTP with a graphical client like FileZilla or WinSCP is more convenient:
# Command-line SFTP
sftp user@your-vps-ip
sftp> cd /media/library/movies
sftp> put -r "Movie Name (2024)"
sftp> exit
Rclone (For Cloud Storage Sources)
If your media is in cloud storage (Google Drive, Dropbox, etc.), rclone can transfer it directly to your VPS without downloading to your local machine first:
# Install rclone on the VPS
sudo apt install rclone
# Configure a remote (interactive)
rclone config
# Copy from cloud to VPS
rclone copy gdrive:Media/Movies /media/library/movies --progress
After uploading, trigger a library scan in Jellyfin: go to Dashboard → Libraries and click Scan All Libraries, or use the API:
curl -X POST "http://127.0.0.1:8096/Library/Refresh" \
-H "X-Emby-Token: your-api-key"
Nginx Reverse Proxy with SSL
Set up Nginx to serve Jellyfin over HTTPS. This provides SSL encryption, proper domain access, and the ability to add security headers.
Create the Nginx configuration:
sudo nano /etc/nginx/sites-available/media.yourdomain.com
server {
listen 80;
server_name media.yourdomain.com;
# Increase max upload size for media uploads via web interface
client_max_body_size 20G;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
location / {
proxy_pass http://127.0.0.1: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;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $host;
# Disable buffering for streaming
proxy_buffering off;
}
# WebSocket support for SyncPlay and real-time updates
location /socket {
proxy_pass http://127.0.0.1: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;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Enable and test:
sudo ln -s /etc/nginx/sites-available/media.yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Get your SSL certificate:
sudo certbot --nginx -d media.yourdomain.com
For the full SSL setup walkthrough, see our Let's Encrypt SSL guide. For more on reverse proxy configuration, see our Nginx reverse proxy guide.
After Certbot completes, visit https://media.yourdomain.com in your browser. You should see the Jellyfin login page.
Update the Jellyfin server URL in Dashboard → Networking: set the "Base URL" field if you're serving from a subpath, or leave it empty if Jellyfin is at the domain root.
Transcoding Configuration and Performance
Transcoding converts media from one format/resolution to another in real time. This happens when a client can't play the original format directly — for example, playing an MKV file with DTS audio on a device that only supports AAC.
Navigate to Dashboard → Playback → Transcoding to configure:
Software Transcoding (CPU-Based)
Software transcoding uses your CPU and is available on every VPS. Configure it in Jellyfin's dashboard:
- Hardware acceleration: None (for software transcoding)
- Thread count: Set to 0 (auto-detect) or your vCPU count
- Transcoding temp path: Leave as default
A rough guide to CPU requirements for software transcoding:
| Resolution | Codec | vCPUs Needed (Per Stream) |
|---|---|---|
| 1080p → 720p | H.264 | 2-4 vCPU |
| 4K → 1080p | H.264 | 6-8 vCPU |
| 4K → 1080p | H.265/HEVC | 8-12 vCPU |
| Any (audio only) | AAC/MP3 | 0.5 vCPU |
Need dedicated CPU for transcoding? Video transcoding is the most CPU-intensive workload you'll run on a VPS. Dedicated CPU ensures smooth playback without contention from other tenants on shared infrastructure. If you're transcoding 4K content, dedicated resources are essential.
Minimizing Transcoding
The best transcoding strategy is to avoid transcoding. Encode your media in formats that most clients can direct-play:
- Video codec: H.264 (widest compatibility) or H.265 (better compression, less compatible)
- Audio codec: AAC stereo (universal compatibility) — include alongside surround tracks
- Container: MKV or MP4
- Resolution: 1080p is the sweet spot for streaming over the internet
You can pre-transcode your library offline using FFmpeg to avoid real-time transcoding entirely:
# Convert a file to a more compatible format
ffmpeg -i input.mkv \
-c:v libx264 -crf 20 -preset slow \
-c:a aac -b:a 192k \
-c:s copy \
output.mkv
Install FFmpeg on your VPS for pre-processing:
sudo apt install ffmpeg
Hardware Acceleration
If your VPS provides GPU or hardware acceleration capabilities, you can dramatically reduce CPU usage for transcoding. Jellyfin supports several hardware acceleration methods:
- VA-API — Intel integrated GPU (most common on dedicated servers)
- NVENC — NVIDIA GPU
- QSV — Intel Quick Sync Video
To use VA-API with an Intel GPU, first verify the device is available:
ls -la /dev/dri/
# Should show renderD128
Then uncomment the device passthrough in your Docker Compose file:
devices:
- /dev/dri:/dev/dri
group_add:
- "109" # render group ID — verify with: getent group render
In Jellyfin's transcoding settings, set Hardware acceleration to Video Acceleration API (VA-API) and configure the VA-API device as /dev/dri/renderD128.
Restart the container after making changes:
cd /opt/jellyfin
sudo docker compose down
sudo docker compose up -d
Note that hardware acceleration availability depends entirely on your hosting provider and VPS type. Standard cloud VPS instances typically don't have GPU access — this is more relevant for dedicated servers or GPU-enabled instances.
User Accounts and Access Control
Create user accounts for family members or friends who'll use your Jellyfin server.
Navigate to Dashboard → Users → Add User:
- Username and password: Set a strong password for each user
- Library access: Choose which libraries each user can see (e.g., kids can access only the Kids library)
- Parental controls: Set maximum parental rating, block specific tags
- Playback limits: Restrict remote bitrate to control bandwidth usage
- Download permissions: Allow or block downloading media files
For bandwidth management across multiple users, set per-user remote streaming bitrate limits:
# Recommended remote streaming bitrates:
# 720p: 4 Mbps
# 1080p: 8 Mbps
# 1080p (high quality): 15 Mbps
# 4K: 40 Mbps (not recommended for remote streaming)
Configure these in each user's settings under Playback → Internet streaming quality.
Bandwidth Considerations and Quality Settings
Streaming media requires consistent bandwidth. Calculate your needs:
| Quality | Bitrate | Bandwidth per Stream | Monthly Data (8 hrs/day) |
|---|---|---|---|
| 720p | 4 Mbps | ~0.5 MB/s | ~432 GB |
| 1080p | 8 Mbps | ~1 MB/s | ~864 GB |
| 1080p High | 15 Mbps | ~1.9 MB/s | ~1.6 TB |
| 4K | 40 Mbps | ~5 MB/s | ~4.3 TB |
Set a global remote streaming limit in Dashboard → Playback to prevent any single user from consuming all your bandwidth. A limit of 8-15 Mbps per stream is a good balance between quality and bandwidth conservation.
To monitor bandwidth usage, check your Nginx access logs or use a monitoring tool. See our monitoring setup guide for comprehensive server monitoring.
Mobile and TV App Setup
Jellyfin has official and third-party clients for every major platform:
Mobile
- Android: Jellyfin for Android (Google Play or F-Droid)
- iOS: Swiftfin (App Store) — community-maintained, actively developed
TV and Streaming Devices
- Android TV / Fire TV: Jellyfin for Android TV (Google Play or sideload)
- Roku: Jellyfin for Roku (Roku Channel Store)
- LG WebOS / Samsung Tizen: Available as installable web apps
- Apple TV: Swiftfin (App Store)
Desktop
- Web browser: Access directly at
https://media.yourdomain.com - Jellyfin Media Player: Desktop app based on MPV (Windows, macOS, Linux)
In each client, set the server address to https://media.yourdomain.com and log in with your Jellyfin credentials. The apps will discover all your libraries and begin syncing metadata.
Storage Management and Expansion
Media libraries grow over time. Here's how to manage storage effectively on your VPS.
Monitor Disk Usage
# Overall disk usage
df -h /media
# Breakdown by library
du -sh /media/library/movies
du -sh /media/library/shows
du -sh /media/library/music
# Find the largest files
du -ah /media/library | sort -rh | head -20
Set Up Storage Alerts
Create a simple monitoring script that alerts you when storage is getting low:
sudo nano /opt/jellyfin/check-storage.sh
#!/bin/bash
THRESHOLD=90 # Alert when disk usage exceeds 90%
USAGE=$(df /media --output=pcent | tail -1 | tr -d ' %')
if [ "$USAGE" -ge "$THRESHOLD" ]; then
echo "WARNING: Media storage at ${USAGE}% capacity on $(hostname)" | \
mail -s "Jellyfin Storage Alert" admin@yourdomain.com
fi
sudo chmod +x /opt/jellyfin/check-storage.sh
# Run every 6 hours
echo "0 */6 * * * /opt/jellyfin/check-storage.sh" | sudo tee -a /var/spool/cron/crontabs/root
For more on cron scheduling, see our cron jobs guide.
Optimizing Storage
Reduce storage usage without losing quality:
# Check the codec and bitrate of a file
ffprobe -v quiet -show_entries format=duration,size,bit_rate -show_entries stream=codec_name,width,height,bit_rate input.mkv
# Re-encode a file with lower bitrate (CRF 23 is visually transparent for most content)
ffmpeg -i input.mkv -c:v libx264 -crf 23 -preset medium -c:a copy output.mkv
# Batch re-encode all MKV files in a directory
find /media/library/movies -name "*.mkv" -exec ffprobe -v quiet -show_entries format=bit_rate {} \;
A script to identify files that would benefit from re-encoding (files with excessively high bitrates):
sudo nano /opt/jellyfin/find-large-files.sh
#!/bin/bash
echo "Files with bitrate over 15 Mbps:"
find /media/library -type f \( -name "*.mkv" -o -name "*.mp4" -o -name "*.avi" \) | while read -r file; do
bitrate=$(ffprobe -v quiet -show_entries format=bit_rate -of csv=p=0 "$file" 2>/dev/null)
if [ -n "$bitrate" ] && [ "$bitrate" -gt 15000000 ]; then
size=$(du -sh "$file" | cut -f1)
echo " $size $(( bitrate / 1000000 )) Mbps $file"
fi
done | sort -rh
Backing Up Jellyfin Configuration
Your media files are large and presumably backed up elsewhere — but your Jellyfin configuration (user accounts, library settings, watch history, metadata) should be backed up regularly.
sudo nano /opt/jellyfin/backup-config.sh
#!/bin/bash
BACKUP_DIR="/opt/jellyfin/backups"
RETENTION_DAYS=30
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
CONFIG_VOLUME=$(docker volume inspect jellyfin-config --format '{{ .Mountpoint }}')
mkdir -p "$BACKUP_DIR"
# Backup the config volume (excludes cache and media)
tar -czf "$BACKUP_DIR/jellyfin-config_$TIMESTAMP.tar.gz" \
-C "$CONFIG_VOLUME" .
# Remove old backups
find "$BACKUP_DIR" -name "jellyfin-config_*.tar.gz" -mtime +$RETENTION_DAYS -delete
echo "Config backup completed: jellyfin-config_$TIMESTAMP.tar.gz ($(du -sh "$BACKUP_DIR/jellyfin-config_$TIMESTAMP.tar.gz" | cut -f1))"
sudo chmod +x /opt/jellyfin/backup-config.sh
# Add to crontab — run weekly
sudo crontab -e
Add:
0 4 * * 0 /opt/jellyfin/backup-config.sh >> /var/log/jellyfin-backup.log 2>&1
For a comprehensive backup strategy, see our automatic backups guide.
Updating Jellyfin
Keep Jellyfin up to date for bug fixes and new features:
# Backup config before updating
sudo /opt/jellyfin/backup-config.sh
# Pull the latest image
cd /opt/jellyfin
sudo docker compose pull
# Restart with the new version
sudo docker compose down
sudo docker compose up -d
# Check the version
sudo docker compose logs jellyfin | head -10
Troubleshooting Common Issues
- Buffering during playback: Check if the client is transcoding. In the Jellyfin dashboard under Active Streams, you can see if a stream is being direct-played or transcoded. If transcoding is the issue, either encode your media in a more compatible format or upgrade your CPU resources
- Metadata not loading: Ensure your file naming follows the conventions above. You can also manually identify media by editing the item in the Jellyfin web interface
- Client can't connect: Verify your Nginx configuration, check that the SSL certificate is valid, and ensure the Docker container is running
- High CPU usage: Almost always transcoding. Check active streams in the dashboard. Consider setting user-level bitrate limits or re-encoding problematic files
Check logs for detailed error information:
# Jellyfin container logs
sudo docker compose logs -f jellyfin
# Resource usage
docker stats jellyfin --no-stream
For system-wide monitoring, see our VPS monitoring guide. For log management best practices, see our log management guide.
Want managed media infrastructure? If you'd rather not manage server updates, storage scaling, and performance tuning yourself, MassiveGRID's managed dedicated cloud servers give you the performance of bare metal with the convenience of full management — we handle the infrastructure so you can focus on your media library.
Final Thoughts
Running Jellyfin on a VPS gives you a personal streaming service with none of the compromises of commercial platforms. There's no subscription fee, no content injection, no telemetry, and no limits on users or features. Your media is served from datacenter-grade infrastructure with consistent bandwidth, accessible from any device, anywhere in the world.
Start with a MassiveGRID Cloud VPS sized for your library and streaming needs. Focus on direct play to minimize CPU requirements, and scale up to a dedicated VPS only if you need frequent transcoding. With the Nginx reverse proxy, SSL, and backup configuration covered in this guide, you have a production-ready media server that's ready for daily use.
For related infrastructure, check out our guide on securing your Ubuntu VPS and optimizing VPS performance to ensure your media server runs smoothly and securely.