Caption: Jellyfin turns your own media library, Docker host, reverse proxy, client apps, metadata providers, and backup storage into a private streaming service.
Introduction
Jellyfin is a free and open-source media server for people who want a private alternative to hosted streaming libraries and proprietary home media platforms. It organizes movies, TV shows, music, photos, home videos, and other media into browser and app-friendly libraries. Users can stream through the web interface, mobile apps, TV clients, Chromecast-style devices, and other compatible players while the server handles metadata, artwork, user accounts, watched status, subtitles, and optional transcoding.
Self-hosting Jellyfin is attractive because media collections are personal. Your library might include ripped discs, home videos, family recordings, downloaded lectures, music archives, or private training content. Running the server yourself lets you decide where files live, who can see them, whether remote access is allowed, how backups are retained, and when upgrades happen. It also avoids the account lock-in and telemetry assumptions that often come with hosted media ecosystems.
This guide installs Jellyfin on Ubuntu 24.04 LTS with Docker Compose using the official jellyfin/jellyfin container image. The example publishes Jellyfin at https://jellyfin.example.com through Nginx, keeps container configuration under /opt/jellyfin, mounts media from /srv/media read-only, and includes practical checks for permissions, HTTPS, streaming, transcoding, and backups. Replace example paths and hostnames before running the commands on your own server.
Why Choose Jellyfin?
- Fully open source: Jellyfin is community-developed and does not require a paid server account.
- Private media ownership: Your video, music, photo, and metadata files stay on infrastructure you control.
- Broad client support: Web browsers, mobile devices, smart TVs, streaming boxes, and desktop clients can connect to the server.
- Docker-friendly deployment: The official container image is available from Docker Hub and GitHub Container Registry.
- User and library controls: Administrators can create users, restrict libraries, manage remote access, and tune playback settings.
- Transcoding support: Jellyfin can use software transcoding or supported GPU acceleration when a client cannot direct-play a file.
- Good homelab fit: It works well on a single Linux server, NAS-style host, or VPS with attached storage when sizing and bandwidth are realistic.
Jellyfin is not a magic replacement for storage planning. A reliable media server still needs clean file organization, correct permissions, enough network throughput, monitored disk usage, and backups for irreplaceable media. The application makes streaming pleasant, but your host remains responsible for durability.
Prerequisites
Hardware Recommendations:
- 2 CPU cores and 2 GB RAM minimum for a small direct-play library
- 4 CPU cores and 4-8 GB RAM recommended if multiple users stream or the server transcodes video
- SSD storage for
/opt/jellyfin/configand/opt/jellyfin/cache - Media storage sized for your library, such as a mounted disk, ZFS dataset, NAS mount, or dedicated
/srv/mediapath - Optional Intel, AMD, or NVIDIA GPU if you need hardware accelerated transcoding
- Off-server backup storage for configuration, metadata, and any media that cannot be recreated
Software and Accounts:
- Ubuntu 24.04 LTS server with sudo access
- A domain such as
jellyfin.example.com - DNS
AorAAAArecord pointing the hostname to the server - Docker Engine with the Docker Compose plugin
- Nginx, Certbot, curl, OpenSSL, ufw, jq, and rsync
- A password manager for administrator credentials and recovery notes
Security Notes:
- Publish Jellyfin through HTTPS when it is reachable outside your home network.
- Do not expose UDP discovery ports to the internet. Client discovery is for local networks only.
- Consider using a VPN or private tunnel instead of public access if only family or internal users need it.
- Keep media mounts read-only unless Jellyfin must write into those folders for a specific workflow.
- Create separate user accounts and avoid sharing the administrator login for normal watching.
- Back up
/opt/jellyfin/config; it contains server settings, users, metadata, watched state, and plugin configuration.
Start with an updated host and a tight firewall:
sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg openssl ufw nginx certbot python3-certbot-nginx jq rsync
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
Installation Guide
This deployment stores Jellyfin application data under /opt/jellyfin and media under /srv/media. Docker listens only on 127.0.0.1:8096 for web traffic, then Nginx handles public HTTPS. UDP discovery on port 7359 is useful only on trusted local networks, so leave it disabled on public VPS deployments unless you understand the network boundary.
1. Install Docker Engine
Install Docker and confirm that Compose v2 is available:
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
newgrp docker
docker --version
docker compose version
If docker compose version fails, fix Docker before continuing. The old docker-compose Python command is not the target for this guide.
2. Create the Jellyfin User and Directories
Create a dedicated system user, application directories, and media directories:
sudo useradd --system --home /opt/jellyfin --shell /usr/sbin/nologin jellyfin || true
sudo mkdir -p /opt/jellyfin/config /opt/jellyfin/cache /opt/jellyfin/fonts
sudo mkdir -p /srv/media/movies /srv/media/tv /srv/media/music /srv/media/home-videos
sudo chown -R jellyfin:jellyfin /opt/jellyfin
sudo chown -R "$USER":"$USER" /srv/media
sudo chmod 750 /opt/jellyfin
sudo chmod -R u=rwX,g=rX,o= /srv/media
For a real library, mount your media disk before adding files. If /srv/media is a network share, make sure it is mounted before Docker starts and that Linux ownership maps to a user Jellyfin can read.
3. Create the Compose Project
Create a private project directory:
sudo mkdir -p /opt/jellyfin-compose
sudo chown "$USER":"$USER" /opt/jellyfin-compose
chmod 700 /opt/jellyfin-compose
cd /opt/jellyfin-compose
cat > .env <<EOF
JELLYFIN_UID=$(id -u jellyfin)
JELLYFIN_GID=$(id -g jellyfin)
TZ=Etc/UTC
JELLYFIN_URL=https://jellyfin.example.com
EOF
chmod 600 .env
Now create docker-compose.yml:
services:
jellyfin:
image: jellyfin/jellyfin:10
container_name: jellyfin
user: "${JELLYFIN_UID}:${JELLYFIN_GID}"
ports:
- "127.0.0.1:8096:8096/tcp"
volumes:
- /opt/jellyfin/config:/config
- /opt/jellyfin/cache:/cache
- type: bind
source: /srv/media
target: /media
read_only: true
- type: bind
source: /opt/jellyfin/fonts
target: /usr/local/share/fonts/custom
read_only: true
environment:
- TZ=${TZ}
- JELLYFIN_PublishedServerUrl=${JELLYFIN_URL}
restart: unless-stopped
The official image also supports tags such as latest, 10, 10.11, or a specific release. The 10 tag follows the current major series. After your server is stable, consider pinning a specific version and reading release notes before upgrades.
Caption: Docker keeps Jellyfin application state, cache, read-only media mounts, optional fonts, localhost HTTP, and the Nginx HTTPS boundary clearly separated.
4. Start Jellyfin
Validate the Compose file, pull the image, and start the container:
cd /opt/jellyfin-compose
docker compose config
docker compose pull
docker compose up -d
docker compose ps
Check that Jellyfin is reachable locally:
curl -I http://127.0.0.1:8096
docker logs --since=10m jellyfin
If logs mention permission errors, recheck ownership on /opt/jellyfin and read permissions on /srv/media. If the container exits immediately, run docker compose logs jellyfin and fix the first concrete error before adding Nginx.
5. Configure Nginx and HTTPS
Create /etc/nginx/sites-available/jellyfin.example.com:
server {
listen 80;
listen [::]:80;
server_name jellyfin.example.com;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name jellyfin.example.com;
client_max_body_size 20M;
ssl_protocols TLSv1.3 TLSv1.2;
ssl_certificate /etc/letsencrypt/live/jellyfin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/jellyfin.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
add_header X-Content-Type-Options "nosniff";
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 $http_host;
proxy_buffering off;
}
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;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
}
}
Request the certificate first if it does not exist yet:
sudo certbot certonly --nginx -d jellyfin.example.com
sudo ln -s /etc/nginx/sites-available/jellyfin.example.com /etc/nginx/sites-enabled/jellyfin.example.com
sudo nginx -t
sudo systemctl reload nginx
sudo certbot renew --dry-run
Confirm DNS points at the server before requesting the certificate. If you keep Jellyfin private behind a VPN, you can skip public Nginx and access the service through the private address instead.
6. Complete First-Run Setup
Open https://jellyfin.example.com and complete the wizard:
- Choose the display language.
- Create the first administrator account with a unique password.
- Add libraries for Movies, Shows, Music, and Home Videos.
- Point each library at the matching
/media/...path inside the container. - Select metadata language and country preferences.
- Decide whether remote access is allowed.
- Sign in with a normal user account for day-to-day playback.
Before importing a huge library, add a few files and verify scanning, artwork, subtitles, and playback. A small test library helps you find folder naming and permission problems early.
Configuration
Jellyfin configuration happens in the dashboard, but the host layer should be documented too.
Library Organization:
- Keep top-level folders simple:
/srv/media/movies,/srv/media/tv,/srv/media/music, and/srv/media/home-videos. - Prefer read-only mounts for media so accidental application writes cannot damage source files.
- Use clear file names and folders before relying on metadata providers.
- Keep home videos or private training material in a separate library with explicit user permissions.
Networking and Remote Access:
- The web interface uses HTTP on port
8096inside this deployment. - HTTPS is terminated by Nginx, which forwards traffic to
127.0.0.1:8096. - UDP
7359is for local client discovery and should not be exposed to the internet. - Jellyfin can run under a base URL, but some clients and integrations behave better on a dedicated subdomain.
- In Dashboard > Networking, configure known proxies and local network ranges if remote access rules depend on them.
Transcoding and Hardware Acceleration:
Software transcoding works for light use, but high-bitrate video can quickly consume CPU. First aim for direct play by using client-friendly files. If users need transcoding, check the host GPU:
lspci -nn | grep -Ei "3d|display|vga" || true
ls -l /dev/dri || true
For Intel or AMD VA-API on Linux, add the device mapping to the Compose service and recreate the container:
devices:
- /dev/dri:/dev/dri
Then enable the matching hardware acceleration option in Dashboard > Playback. Jellyfin recommends its bundled jellyfin-ffmpeg; the official Docker image includes the expected FFmpeg build.
Usage
After setup, test Jellyfin with real client workflows instead of only checking that the homepage loads.
Caption: A healthy Jellyfin workflow starts with organized media files, then scans libraries, enriches metadata, authenticates users, and streams direct-play or transcoded video to clients.
First Playback Checklist:
cd /opt/jellyfin-compose
docker compose ps
docker logs --since=30m jellyfin
curl -I https://jellyfin.example.com
du -sh /opt/jellyfin/config /opt/jellyfin/cache /srv/media
In the browser or app:
- Scan each library from the dashboard.
- Play one movie or home video in a browser.
- Play one file from a TV, phone, or tablet client.
- Confirm subtitles display correctly.
- Confirm remote access is blocked or allowed according to your plan.
- Create a non-admin user and verify it can only see intended libraries.
- Check active devices in the dashboard while playback is running.
Direct play is usually the best outcome. If playback starts slowly or CPU usage spikes, inspect the media details to see whether Jellyfin is transcoding. A different client app, more compatible file format, or hardware acceleration may solve the problem better than buying a larger server.
Backups and Restore Planning
Back up Jellyfin configuration and irreplaceable media. The most important application path is /opt/jellyfin/config. The cache can usually be rebuilt, but keeping it may reduce restore time.
Caption: Jellyfin backups should protect configuration, users, metadata, watched state, Compose files, and irreplaceable media with restore tests on a separate host.
Create a simple backup staging directory:
sudo mkdir -p /var/backups/jellyfin
sudo chmod 700 /var/backups/jellyfin
Run a manual backup:
sudo tar -C /opt -czf "/var/backups/jellyfin/jellyfin-config-$(date +%F).tar.gz" jellyfin
sudo tar -C /opt -czf "/var/backups/jellyfin/jellyfin-compose-$(date +%F).tar.gz" jellyfin-compose
sudo rsync -a --delete /var/backups/jellyfin/ backup-user@backup.example.com:/srv/backups/jellyfin/
If media files are unique, back them up too. For very large libraries, use a backup tool that supports incremental changes, snapshots, encryption, and retention policies. Good restore planning includes a test: start a temporary Jellyfin container on another machine, restore /opt/jellyfin/config, mount sample media at the same path, and confirm users, libraries, and watched state appear.
Troubleshooting
- Login page does not load: Check
docker compose ps,docker logs jellyfin, andcurl -I http://127.0.0.1:8096before debugging Nginx. - Nginx returns 502: Confirm the container is running and the proxy target is
127.0.0.1:8096. - Certificate request fails: Verify DNS, firewall rules, and that ports
80and443are reachable from the internet. - Libraries scan but stay empty: Recheck media folder paths inside the container and host permissions on
/srv/media. - Remote clients cannot connect: Review Dashboard > Networking, known proxies, remote access settings, and whether the user is allowed remote connections.
- Local discovery does not work: UDP
7359only helps on a trusted local subnet. It will not work across most routed networks or public reverse proxies. - Playback buffers or CPU hits 100 percent: Determine whether the file is direct-playing or transcoding, then adjust client support, media format, bitrate, or hardware acceleration.
- Subtitles burn in slowly: Subtitle burn-in can force transcoding. Try a client that supports the subtitle format directly or configure GPU acceleration.
- After an upgrade, plugins or clients break: Read Jellyfin release notes, update plugins carefully, and keep a backup before recreating containers.
Scaling, Securing, and Next Steps
A single Docker host can run Jellyfin well for a household, small team, or private archive when most playback is direct play and storage is dependable. As usage grows, watch upload bandwidth, disk throughput, cache growth, CPU load during transcodes, and the number of simultaneous streams. Remote users with limited bandwidth can create more transcoding load than local users on a fast LAN.
For security, keep Jellyfin behind HTTPS or VPN, use named users, restrict libraries, disable remote access for accounts that do not need it, and avoid exposing discovery services outside trusted networks. For reliability, document the Compose file, pin versions after testing, keep backups off-server, and rehearse restores before the media library becomes mission-critical.
Following this guide gives you a self-hosted Jellyfin baseline with Docker Compose, Nginx HTTPS, read-only media mounts, first-login checks, optional hardware acceleration notes, and a backup routine. From here, you can add more client apps, tune metadata providers, test GPU transcoding, connect private storage, and gradually invite users once permissions and restore procedures are proven.