Jellyfin Docker Guide: Self-Host a Free Plex Alternative on a VPS

Run Jellyfin on a VPS using Docker Compose. Learn how to configure media libraries, hardware acceleration (transcoding), and secure remote SSL access.

This guide outlines the technical implementation steps to deploy a Jellyfin Media Server on a virtual private server (VPS) using Docker Compose. It covers directory layout, environment configuration, hardware acceleration setup (Intel/Nvidia), and reverse proxy integration with SSL.

Directory Structure Setup

Before running the containers, establish a persistent directory structure on the host system to store application configurations, cache data, and media libraries.

Execute the following commands to create the required directory paths:

sudo mkdir -p /opt/jellyfin/{config,cache}
sudo mkdir -p /mnt/media/{movies,tvshows}

Assign ownership of these directories to the non-root user that will execute the Docker container (replace 1000:1000 with your target user and group IDs):

sudo chown -R 1000:1000 /opt/jellyfin
sudo chown -R 1000:1000 /mnt/media

Docker Compose Configuration

Create a file named docker-compose.yml in your deployment directory (e.g., /opt/jellyfin/). The configuration below maps the persistent storage volumes, configures the network interface, and sets up container behavior.

version: '3.8'

services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    user: "1000:1000" # Explicitly match host PUID and PGID
    environment:
      - JELLYFIN_LOG_DIR=/config/log
    volumes:
      - /opt/jellyfin/config:/config
      - /opt/jellyfin/cache:/cache
      - /mnt/media/movies:/data/movies:ro # Mount media as read-only for security
      - /mnt/media/tvshows:/data/tvshows:ro
    ports:
      - "127.0.0.1:8096:8096" # Bind to localhost to restrict external HTTP traffic
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true

Hardware Acceleration (HWA)

Hardware transcoding reduces CPU utilization when streaming media that requires format conversion.

Intel and AMD GPUs (VA-API / QuickSync)

For Intel or AMD hardware acceleration, pass the render device /dev/dri from the host to the container.

  1. Ensure the user running the container belongs to the video or render group on the host. Verify the group ID of /dev/dri/renderD128: bash ls -l /dev/dri/renderD128
  2. Update the jellyfin service block in docker-compose.yml to include the device node:
    devices:
      - /dev/dri/renderD128:/dev/dri/renderD128
      - /dev/dri/card0:/dev/dri/card0

NVIDIA GPUs (NVENC / NVDEC)

To utilize NVIDIA graphics cards for hardware acceleration, you must install the NVIDIA Container Toolkit on the host VPS and configure the container to use the NVIDIA runtime.

  1. Install the NVIDIA Container Toolkit according to your host OS documentation.
  2. Update the docker-compose.yml configuration:
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

Reverse Proxy and SSL Configuration

To secure the Jellyfin interface with SSL/TLS, configure a reverse proxy to route traffic from port 443 to the container's port 8096.

Caddy automatically handles Let's Encrypt SSL certificate provisioning and renewals.

Create a Caddyfile in /etc/caddy/Caddyfile:

jellyfin.example.com {
    reverse_proxy 127.0.0.1:8096 {
        header_up X-Real-IP {remote_host}
    }
}

Option 2: Nginx

If using Nginx, place the following configuration block inside your server configuration /etc/nginx/sites-available/jellyfin:

server {
    listen 80;
    listen [::]:80;
    server_name jellyfin.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name jellyfin.example.com;

    ssl_certificate /etc/letsencrypt/live/jellyfin.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/jellyfin.example.com/privkey.pem;

    # Security headers
    add_header X-Frame-Options "DENY";
    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;

        # Disable buffering for smoother video playback
        proxy_buffering off;
    }

    # WebSockets support for Jellyfin sync/casting features
    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;
    }
}

Running the Deployment

To start the Jellyfin container, navigate to the directory containing your docker-compose.yml file and run:

docker compose up -d

Verify that the container is running and inspect the startup logs:

docker compose ps
docker compose logs -f jellyfin

Access the Jellyfin setup wizard by navigating to your configured domain (e.g., https://jellyfin.example.com) in a web browser.