Access Jellyfin Remotely Using an Nginx Reverse Proxy

Nginx is an excellent reverse proxy — it can sit in front of your web applications and forward traffic to backend services, adding SSL, caching, rate limiting, and security headers along the way. One of my favorite uses is exposing a Jellyfin media server running at home through a public-facing Nginx server, so I can stream my media library from anywhere.

What You'll Need

  • A VPS or dedicated server running Nginx that's publicly reachable (Linode, DigitalOcean, etc.)
  • Jellyfin installed and running on a machine on your home network (default port 8096)
  • A domain name pointed to your VPS
  • Port 8096 forwarded on your home router to the machine running Jellyfin — without this, Nginx can't reach it and you'll get 502 errors

The Basic Idea

A visitor requests jelly.yourdomain.com. Nginx on your VPS receives the request, connects back to your Jellyfin server at home over the public internet, fetches the response, and passes it back to the visitor. The visitor never knows your home IP address — they only talk to the VPS. Meanwhile, Nginx layers on SSL, security headers, and access controls that Jellyfin might not handle gracefully on its own.


The Configuration

server {
    # Replace with your actual listen directive or include as needed
    listen 443 ssl http2;
    server_name jelly.yourdomain.com;

    # SSL certificates — use Let's Encrypt for free automated certs
    ssl_certificate /etc/letsencrypt/live/jelly/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/jelly/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/jelly/chain.pem;

    # Turn off access logging to keep disks clean (error log stays on)
    access_log off;
    error_log /var/log/nginx/jelly.error.log;

    # Increase upload size for posters, album art, etc.
    client_max_body_size 20M;

    # Store the address of your home Jellyfin server
    # This is your ISP-assigned public IP — the one you see
    # at whatismyip.com from your home network.
    # Port 8096 must be forwarded on your home router to the
    # machine running Jellyfin, or Nginx will get 502 errors.
    # If your IP changes, see the Dynamic IPs section below.
    set $jellyfin 203.0.113.50;

    # ---- Security Headers ----

    # Restrict what browser features the page can use
    add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), "
        "battery=(), bluetooth=(), camera=(), clipboard-read=(), "
        "display-capture=(), document-domain=(), encrypted-media=(), "
        "gamepad=(), geolocation=(), gyroscope=(), hid=(), "
        "idle-detection=(), interest-cohort=(), keyboard-map=(), "
        "local-fonts=(), magnetometer=(), microphone=(), payment=(), "
        "publickey-credentials-get=(), serial=(), sync-xhr=(), "
        "usb=(), xr-spatial-tracking=()" always;

    # Content Security Policy — restricts what can load and from where
    # External JS like Chromecast's cast_sender.js must be whitelisted
    add_header Content-Security-Policy "default-src https: data: blob: ; "
        "img-src 'self' https://* ; "
        "style-src 'self' 'unsafe-inline'; "
        "script-src 'self' 'unsafe-inline' "
        "https://www.gstatic.com https://www.youtube.com blob:; "
        "worker-src 'self' blob:; "
        "connect-src 'self'; "
        "object-src 'none'; "
        "frame-ancestors 'self'; "
        "font-src 'self'";

    # ---- Main Proxy ----
    location / {
        proxy_pass http://$jellyfin: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 $http_host;

        # Disable buffering — streaming can get very resource-heavy otherwise
        proxy_buffering off;
    }

    # ---- WebSocket Proxy (needed by Jellyfin clients) ----
    location /socket {
        proxy_pass http://$jellyfin: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-Host $http_host;
    }

    # Needed for Let's Encrypt certificate renewal
    location ~ /.well-known/acme-challenge {
        root /usr/share/nginx/html;
        allow all;
    }
}

Breaking Down the Key Directives

proxy_pass http://$jellyfin:8096; — This is where the magic happens. Nginx connects back to your home network over the public internet and fetches whatever Jellyfin serves up. The address in $jellyfin should be your home's public IP (find it at whatismyip.com from home). Using a variable rather than hardcoding the IP directly in proxy_pass also makes DNS-based approaches possible (see the dynamic IP note below).

proxy_set_header — These tell Jellyfin what the original request looked like. X-Forwarded-For passes along the visitor's real IP so your Jellyfin logs show actual users, not just your VPS. X-Forwarded-Proto tells Jellyfin the original connection was HTTPS even though Nginx talks to it over plain HTTP internally. X-Forwarded-Host preserves the original hostname.

proxy_buffering off; — Critical for media streaming. When buffering is enabled, Nginx waits to receive the entire backend response before sending it to the client. With video, that means huge memory consumption and stuttering playback. Turn it off.

The WebSocket Bit

Jellyfin uses WebSockets for real-time communication with clients — play/pause events, progress updates, and so on. The /socket location block upgrades the HTTP connection to a WebSocket connection when requested. Without this, some Jellyfin clients will behave strangely or fail to connect entirely.

A Note on Dynamic IPs

Most residential ISPs give you a dynamic IP that changes from time to time. Hardcoding an IP in the config will eventually break. You have a few options:

  • Dynamic DNS (DDNS) — Run a DDNS client at home (many routers have one built in) so a hostname like home.yourdomain.com always points to your current public IP. Then swap set $jellyfin 203.0.113.50; for set $jellyfin home.yourdomain.com; — and add a resolver directive so Nginx re-resolves DNS periodically instead of once at startup. For example: resolver 1.1.1.1 valid=300s; and set $jellyfin home.yourdomain.com;
  • IPv6 — If your ISP provides native IPv6, your home machines likely have static or semi-static public IPv6 addresses. You can skip port forwarding and dynamic DNS entirely by using the Jellyfin machine's IPv6 address directly (though you'll still want firewall rules to restrict access).
  • WireGuard tunnel — Instead of exposing port 8096 to the internet at all, connect your home machine and VPS over a WireGuard VPN. Nginx proxies to the home machine's WireGuard IP (e.g. 10.0.0.2). More secure since no ports need to be open at home beyond WireGuard itself, but a bit more setup.

For most people, DDNS is the simplest approach. Many routers and NAS devices support it out of the box with providers like DuckDNS, Cloudflare, or afraid.org. If you're already using Cloudflare for your domain's DNS, the Cloudflare Real IP with Nginx guide covers the Nginx-side configuration for proxy-aware logging and rate limiting — useful context if you're running both a reverse proxy and web-facing sites on the same server.

Security: Never Use $host in proxy_pass

A dangerous shortcut you might see in some tutorials looks like this:

server {
    listen 80;
    location / {
        # DANGER — do not do this
        proxy_pass http://$host;
    }
}

This takes whatever hostname the visitor requested and forwards traffic to it. Sounds convenient — until someone points a random domain at your server and turns it into an open proxy for abuse, bandwidth theft, or worse. Always hardcode the destination or use a controlled variable like we've done above.

Blocking Unmatched Requests

To prevent your Nginx server from responding to requests for hostnames you haven't configured, add a catch-all block that silently drops them:

server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;
    # 444 closes the connection immediately with no response
    return 444;
}

This catches any request that doesn't match a defined server_name and drops it without giving away any information about your server.

SSL with Let's Encrypt

The configuration above assumes you've already obtained certificates. If you haven't, Certbot makes it easy. The .well-known/acme-challenge location block at the bottom of the server block allows Certbot to verify domain ownership for automated renewals via HTTP challenge. If you'd rather not open port 80 at all, you can use DNS-based validation instead, which proves domain ownership through a TXT record and requires no open inbound ports.

Don't Forget Your Router

All of this assumes your home router has port 8096 forwarded to the machine running Jellyfin. Nginx on your VPS can only proxy to what it can reach — if that port isn't open, you'll see 502 Bad Gateway errors in your browser. For extra security, consider locking down that port forward to only accept connections from your VPS IP address in your router's firewall rules, so only your Nginx server can talk to Jellyfin and not the entire internet.

Going Further

Once you have the basic proxy working, you can layer on additional protections:

  • Rate limiting with limit_req on the VPS to slow down brute-force login attempts before they even reach your home connection — see the Rate Limiting with Nginx's limit_req guide for the full setup
  • Fail2ban on the home machine — since Jellyfin logs live there, Fail2ban can read them directly and block offending IPs at the home firewall level. The fail2ban with nftables guide covers the server-side setup
  • IP allowlists if you only want specific people accessing your server
  • Caching static assets (CSS, JS, poster images) on the VPS to reduce load on your home connection and speed up the web interface

But the config above gives you the foundation — a secure, SSL-terminated reverse proxy for your home media server.

Technical Audit Summary

This guide is maintained as part of a modular, SSL-first framework. Each configuration is audited for production stability and modern security standards.

Last Audit: May 2026
Environment: Debian Trixie (13)
Nginx: 1.30.2

Compatibility: Tested against current stable releases. The reverse proxy configuration works on any Nginx 1.18+ build with the standard ngx_http_proxy_module.

2026-05-21: Production audit — bumped Nginx to 1.30.1. Removed PHP-FPM version from audit (this is a reverse proxy, not a PHP application). Added cross-links to the Cloudflare Real IP guide (for DDNS and proxy-aware Nginx config), the rate-limiting guide, and the fail2ban + nftables guide.