Plex used to be the obvious answer. Then they started gating remote streaming behind a subscription, pushed ads into the home screen, and rolled out a "rental store" nobody asked for. Jellyfin is the open-source fork of Emby that picked up the abandoned MIT-licensed code in 2018, kept the philosophy clean, and now competes feature-for-feature with Plex on the things that actually matter — minus the corporate friction.
This guide walks through deploying Jellyfin on a VPS using Docker, putting it behind an Nginx reverse proxy with Let's Encrypt HTTPS, and configuring it for remote access. We'll also be honest about what a VPS-hosted media server can't do — namely, it can't transcode 4K HEVC on a $5/mo CPU-only plan no matter how many YouTube tutorials say otherwise.
- A Linux VPS (Ubuntu 22.04 or Debian 12 — at least 2 vCPU, 2 GB RAM, NVMe storage)
- A domain name pointed at your VPS (an A record)
- Storage for your media — VPS disk, attached block storage, or remote SMB/NFS share
- Roughly 30 minutes
1. Prepare the VPS
Start with a fresh Ubuntu 22.04 or Debian 12 install. SSH in as root (or a sudo user), then update everything before you do anything else:
apt update && apt upgrade -y
apt install -y ufw curl ca-certificates gnupg
Set up a basic firewall. Jellyfin's web UI runs on port 8096 internally, but we'll only expose 80 and 443 publicly through Nginx:
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
Create a non-root user for Docker management if you haven't already:
adduser jellyfin
usermod -aG sudo jellyfin
rsync --archive --chown=jellyfin:jellyfin ~/.ssh /home/jellyfin
2. Install Docker
Use Docker's official repo rather than the distro version — the distro version is usually outdated and missing Compose v2.
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
usermod -aG docker jellyfin
If you're already using Debian, swap "ubuntu" for "debian" in both URLs above. Verify:
docker --version
docker compose version
3. Deploy Jellyfin
Switch to the jellyfin user and create a working directory:
su - jellyfin
mkdir -p ~/jellyfin/{config,cache,media}
cd ~/jellyfin
Drop a compose.yaml into ~/jellyfin/:
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
user: 1000:1000
network_mode: host
volumes:
- ./config:/config
- ./cache:/cache
- ./media:/media
environment:
- JELLYFIN_PublishedServerUrl=https://media.example.com
restart: unless-stopped
The network_mode: host is intentional — it lets Jellyfin auto-discover on your LAN if you ever connect through WireGuard, and avoids Docker NAT slowdown for streaming. The user: 1000:1000 matches the jellyfin user's UID/GID so file permissions on ./media stay sane.
Start it up:
docker compose up -d
docker compose logs -f
You should see Application startup finished. Running within 20–30 seconds. The web UI is now live on port 8096, but only locally — we haven't exposed it yet.
4. Reverse proxy with Nginx + HTTPS
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginx
Drop the Jellyfin-specific Nginx config into /etc/nginx/sites-available/jellyfin:
server {
listen 80;
server_name media.example.com;
client_max_body_size 20M;
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "SAMEORIGIN";
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-Host $host;
proxy_buffering off;
}
# Websocket support for live 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 it and grab a cert:
sudo ln -s /etc/nginx/sites-available/jellyfin /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d media.example.com
Certbot will auto-rewrite your config to listen on 443 with HTTP/2, redirect 80 → 443, and set up a renewal cron. Visit https://media.example.com and you should land on Jellyfin's first-run wizard.
🐾 Need a VPS that won't choke on 1080p streams?
OliveVPS plans start at $3.99/mo with NVMe storage and dedicated CPU on the Pro plan and up. No transcoding ceiling on the 1080p direct-play path. Pick a region close to wherever you'll be streaming from most.
Get Started →5. Add your media libraries
Walk through the first-run wizard: create your admin account, then add a library. For each library type (Movies, Shows, Music), point Jellyfin at the right folder under /media inside the container.
For naming, follow the Jellyfin docs naming conventions:
/media/movies/The Matrix (1999)/The Matrix (1999).mkv
/media/shows/Breaking Bad/Season 01/Breaking Bad - S01E01.mkv
/media/music/Artist/Album/01 - Track.flac
Jellyfin uses these names to fetch metadata from TheMovieDB, TheTVDB, and MusicBrainz. Get the names wrong and you'll get an empty library and a lot of confused troubleshooting.
Getting media onto the VPS
You have three realistic options:
- SFTP/rsync from your computer. Works for small libraries.
rsync -avz --progress ~/Movies/ jellyfin@media.example.com:~/jellyfin/media/movies/ - VPS-side download tools. qBittorrent or NZBGet running on the same VPS, with completed downloads going straight into the Jellyfin media folder.
- Mount remote storage. If your library is too big for VPS disk (most are — 1 TB of movies is real money in cloud storage), mount a Backblaze B2 or Wasabi bucket via rclone, or NFS-mount your home server. Streaming reads will be slower; expect occasional buffering on the first play.
6. The transcoding reality check
Here is the part most tutorials lie about. Transcoding is expensive. When Jellyfin can't direct-play a file (because the client doesn't support the codec, the bitrate is too high for the connection, or you're using subtitles burned-in), it has to decode and re-encode in real time.
On VPS hardware without a GPU, this hits the CPU hard:
| Source | Target | CPU needed | Realistic on VPS? |
|---|---|---|---|
| 1080p H.264 | 720p H.264 | 1–2 cores | Yes — most plans |
| 1080p H.265 (HEVC) | 1080p H.264 | 3–4 cores | Maybe — Pro plan and up |
| 4K HEVC HDR | 1080p SDR | 8+ cores or GPU | No — get a dedicated server |
| 4K HEVC HDR | Direct-play | ~0 | Yes — if client supports it |
The trick is to avoid transcoding entirely. Use modern clients (Jellyfin Media Player, Findroid on Android, Swiftfin on iOS, Jellyfin for Apple TV) — these all support direct-play of HEVC and most modern codecs. Direct-play sends the file as-is and your CPU stays at 5%.
If you genuinely need transcoding and your VPS provider supports it, look for plans with Intel Quick Sync (most modern Xeon / E-2xxx CPUs have it) and pass through /dev/dri to the container. On most cloud VPS hardware, hardware transcoding isn't available — you'll need a dedicated server.
7. Security and access control
A media server exposed to the internet is a tempting target. The mitigations:
Disable user signup. In Dashboard → General, uncheck "Allow new users to create accounts."
Require strong passwords. The default password policy is weak. Set a minimum length of 12 characters under Dashboard → Users → Default User Policy.
Add fail2ban. Brute-force protection for the login endpoint:
sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.d/jellyfin.conf <<EOF
[jellyfin]
enabled = true
port = http,https
filter = jellyfin
logpath = /home/jellyfin/jellyfin/config/log/log_*.log
maxretry = 5
bantime = 3600
EOF
Then create the filter at /etc/fail2ban/filter.d/jellyfin.conf:
[Definition]
failregex = ^.*Authentication request for .* has been denied \(IP: <HOST>\)\.$
ignoreregex =
sudo systemctl restart fail2ban
Consider WireGuard-only access. If the server is for you and a small group, skip public exposure entirely. Run WireGuard on the same VPS, route Jellyfin to listen only on the WG interface, and connect from devices through the VPN. This eliminates 99% of attack surface.
8. Common problems
"Jellyfin won't load — connection times out" — UFW is blocking 80/443, or Nginx isn't running. Check sudo systemctl status nginx and sudo ufw status.
"Subtitles cause buffering" — Burned-in subtitles trigger a transcode. Use external .srt files instead of mkv-embedded forced subs, and pick a client that supports SSA/ASS rendering natively.
"Library scan is stuck" — Check folder permissions. The container runs as UID 1000; if your media files are owned by root, Jellyfin can't read them. chown -R 1000:1000 ~/jellyfin/media.
"Remote streams keep buffering" — Bandwidth is the issue, not CPU. Check your VPS's upload bandwidth (why this matters) and the client's connection. 1080p needs ~10–20 Mbps sustained.
"Hardware transcoding flag not appearing" — VPS likely doesn't expose /dev/dri. This is normal on shared hardware. Move to a dedicated server with a passthrough GPU, or stick with direct-play.
FAQ
Is Jellyfin really as good as Plex?
For 90% of use cases, yes. Jellyfin matches Plex on library management, metadata fetching, transcoding, and client app coverage (officially or via community apps like Findroid and Swiftfin). What it doesn't have: Plex's polish in onboarding, the "Plex pass" feature set bundled together, and a smooth experience for non-technical users you've shared with. Pick Plex if your spouse needs a one-tap experience. Pick Jellyfin if you want full control and zero subscription gates.
How much storage do I really need?
Rough numbers: 1080p Blu-ray movies are 8–25 GB each, 4K HDR titles are 40–80 GB, and a TV show season averages 15–60 GB. For a curated library of ~200 movies and ~30 shows, expect 2–4 TB. VPS-attached storage gets expensive at that scale — most people keep the bulk on a home NAS or object storage and run Jellyfin on the VPS as just the streaming endpoint.
Will my ISP throttle Jellyfin streams?
Some ISPs throttle high-bandwidth video flows. Because Jellyfin streams over HTTPS on port 443, it generally looks like normal web traffic and goes through unmolested. If you do hit issues, layering WireGuard on top eliminates DPI inspection entirely.
Can I share with friends and family?
Yes — create separate Jellyfin user accounts for each person, set their library access scope under user policies, and give them the URL plus client apps. Bandwidth-wise, every concurrent 1080p stream is about 15 Mbps egress; if you're hosting on a small VPS, 3–4 simultaneous streams will saturate most plans. Track your egress (our bandwidth explainer).
Do I need a domain name, or can I use the IP directly?
You technically can use IP-only, but you won't get HTTPS without painful self-signed cert workarounds, and most modern clients refuse plain HTTP. A domain costs $10–15/year and makes the entire setup work cleanly. Use any registrar; point an A record at your VPS IP.
What about backups?
The media itself is replaceable (you can re-rip or re-download). What you actually want to back up is ~/jellyfin/config — that holds your watch progress, user accounts, and library metadata. A nightly tar to remote storage is enough: tar czf - ~/jellyfin/config | restic backup --stdin.