Set up Jellyfin + Nginx + Cloudflare Tunnel (HTTPS)

This guide sets up Jellyfin in Docker on Ubuntu, with:

  • Optional NVIDIA hardware transcoding
  • Secure public access via Nginx + Cloudflare Tunnel
  • No exposed ports to the internet

Architecture Overview

Internet
  ↓
Cloudflare Tunnel
  ↓
Nginx (HTTPS, Cloudflare Origin Cert)
  ↓
Jellyfin (Docker, localhost only)

What you’ll end up with

  • Jellyfin running locally at 127.0.0.1:8096
  • Optional GPU-accelerated transcoding
  • Public access at https://watch.example.com
  • No inbound firewall rules required

Prerequisites

  • Ubuntu 22.04 / 24.04
  • A Cloudflare-managed domain
  • (Optional) NVIDIA GPU supported by NVENC

Install Docker

Install Docker from the official repository:

sudo apt update
sudo apt install -y ca-certificates curl gnupg

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo 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" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker

Optional: allow Docker without sudo:

sudo usermod -aG docker $USER
# log out & back in

Prepare Jellyfin directories

mkdir -p /home/henry/jellyfin/{config,cache,media,fonts}
cd /home/henry/jellyfin

Docker Compose configuration

Create docker-compose.yml:

services:
  jellyfin:
    image: jellyfin/jellyfin
    container_name: jellyfin
    ports:
      - "127.0.0.1:8096:8096"
      - "7359:7359/udp"
    volumes:
      - /home/henry/jellyfin/config:/config
      - /home/henry/jellyfin/cache:/cache
      - /home/henry/jellyfin/media:/media
      - /home/henry/jellyfin/fonts:/usr/local/share/fonts/custom:ro
    restart: unless-stopped

    # NVIDIA GPU support (safe even if GPU is not installed)
    runtime: nvidia
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

Docker will ignore the GPU section unless NVIDIA support is installed.

NVIDIA GPU support (optional)

Skip this section if you only want CPU transcoding.

Install NVIDIA driver

sudo ubuntu-drivers install
reboot

Verify:

nvidia-smi

Install NVIDIA Container Toolkit

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | sudo gpg --dearmor -o /etc/apt/keyrings/nvidia-container-toolkit.gpg

curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
  | sed 's#deb https://#deb [signed-by=/etc/apt/keyrings/nvidia-container-toolkit.gpg] https://#g' \
  | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt update
sudo apt install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

Verify Docker GPU access:

docker run --rm --gpus all \
  nvidia/cuda:12.3.2-base-ubuntu22.04 nvidia-smi

Start Jellyfin

docker compose up -d

Verify:

curl -I http://127.0.0.1:8096

Jellyfin initial configuration

  • Open http://127.0.0.1:8096
  • Create admin user
  • Add media library at /media

Enable hardware transcoding (GPU only)

Dashboard → Playback → Transcoding

Enable:

  • Hardware acceleration: NVIDIA NVENC
  • Hardware decoding
  • Allow encoding: H264 / HEVC
  • Tone mapping (optional)

Save.

Verify GPU transcoding

  1. Play a file that forces transcoding
  2. Open Playback Info

    • Play method: Transcoding
    • FPS: typically hundreds

On the host:

watch -n 1 nvidia-smi

Public access with Cloudflare Tunnel

Example domain: watch.example.com Replace everywhere with your real hostname.

Cloudflare dashboard setup

  1. Create a Cloudflare Origin Certificate

    • Cloudflare → SSL/TLS → Origin Server → Create certificate
    • Hostnames: watch.example.com (optionally also *.example.com)
    • Copy the Origin Certificate (PEM) and Private Key
  2. Set SSL mode

    • Cloudflare → SSL/TLS → Overview
    • Set Encryption mode = Full (strict)
  3. DNS (Tunnel uses a CNAME, not IPv4)

    • Do NOT create an A record pointing to your server IP/Tailscale IP.
    • You will run cloudflared tunnel route dns ... later; it creates/updates a **CNAME → .cfargotunnel.com** (proxied).

Nginx reverse proxy (HTTPS)

Install Nginx

sudo apt update
sudo apt install -y nginx

Install Cloudflare Origin Certificate

sudo mkdir -p /etc/ssl/cloudflare
sudo nano /etc/ssl/cloudflare/jellyfin.pem   # paste Cloudflare Origin Certificate (-----BEGIN CERTIFICATE----- ...)
sudo nano /etc/ssl/cloudflare/jellyfin.key   # paste Cloudflare Private Key (-----BEGIN PRIVATE KEY----- ...)
sudo chmod 600 /etc/ssl/cloudflare/jellyfin.key
sudo chmod 644 /etc/ssl/cloudflare/jellyfin.pem

Nginx site configuration

Create /etc/nginx/sites-available/jellyfin:

sudo nano /etc/nginx/sites-available/jellyfin

Use this example config (IMPORTANT: apt nginx uses listen ... http2; not http2 on;):

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

server {
    listen 443 ssl http2;
    server_name watch.example.com;

    client_max_body_size 50G;

    ssl_certificate     /etc/ssl/cloudflare/jellyfin.pem;
    ssl_certificate_key /etc/ssl/cloudflare/jellyfin.key;

    location / {
        proxy_pass http://127.0.0.1:8096;

        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_read_timeout 3600s;
        proxy_buffering off;
    }
}

Enable and restart:

sudo ln -s /etc/nginx/sites-available/jellyfin /etc/nginx/sites-enabled/jellyfin
sudo nginx -t
sudo systemctl restart nginx

Cloudflare Tunnel

Install cloudflared (Ubuntu 24.04: use GitHub .deb)

Confirm arch (expect x86_64):

uname -m

Install cloudflared:

wget -O cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
sudo apt-get -f install -y
cloudflared --version

Create Cloudflare Tunnel + DNS route

Login (approve in browser using the printed URL):

cloudflared tunnel login
# saves: ~/.cloudflared/cert.pem

Create tunnel:

cloudflared tunnel create jellyfin
# creates: ~/.cloudflared/<tunnel-uuid>.json   <-- IMPORTANT CREDENTIALS FILE

Route DNS (creates/updates CNAME record for watch.example.com):

cloudflared tunnel route dns jellyfin watch.example.com

If it says a record already exists, delete/replace the existing DNS record for watch.example.com in Cloudflare DNS, then rerun the command.

Configure cloudflared ingress to point to Nginx HTTPS (origin)

Create config:

sudo mkdir -p /etc/cloudflared
sudo nano /etc/cloudflared/config.yml

Config template:

tunnel: jellyfin
credentials-file: /etc/cloudflared/jellyfin.json

ingress:
  - hostname: watch.example.com
    service: https://127.0.0.1:443
    originRequest:
      noTLSVerify: true
  - service: http_status:404

Copy the tunnel credentials JSON to the path referenced above:

ls -la ~/.cloudflared
# you should see a file like: <tunnel-uuid>.json

sudo cp ~/.cloudflared/*.json /etc/cloudflared/jellyfin.json
sudo chown root:root /etc/cloudflared/jellyfin.json
sudo chmod 600 /etc/cloudflared/jellyfin.json
sudo ls -la /etc/cloudflared/

Install & start cloudflared as a systemd service

Install service:

sudo cloudflared service install

Enable + start:

sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared --no-pager -l

Logs:

sudo journalctl -u cloudflared -n 200 --no-pager

Troubleshooting quick fixes

1. Cloudflare 502 / “Bad Gateway”

Check cloudflared logs:

sudo journalctl -u cloudflared -n 200 --no-pager

If you see: dial tcp 127.0.0.1:443: connect: connection refused then Nginx isn’t listening on 443. Fix:

sudo nginx -t
sudo systemctl restart nginx
sudo ss -lntp | grep ':443' || true
curl -kI https://127.0.0.1:443 -H "Host: watch.example.com"

2. cloudflared service won’t start

Almost always missing credentials file:

  • /etc/cloudflared/jellyfin.json

Fix by copying JSON again:

sudo cp ~/.cloudflared/*.json /etc/cloudflared/jellyfin.json
sudo chown root:root /etc/cloudflared/jellyfin.json
sudo chmod 600 /etc/cloudflared/jellyfin.json
sudo systemctl restart cloudflared
sudo systemctl status cloudflared --no-pager -l



Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Fixing Linux Boot Failure Due to fstab Mount Errors
  • Enable Home PC (WSL2) to Act as a Server