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
- Play a file that forces transcoding
-
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.comReplace everywhere with your real hostname.
Cloudflare dashboard setup
-
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
-
Set SSL mode
- Cloudflare → SSL/TLS → Overview
- Set Encryption mode = Full (strict)
-
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: