Jellyfin Forum
Nginx reverse proxy - Printable Version

+- Jellyfin Forum (https://forum.jellyfin.org)
+-- Forum: Support (https://forum.jellyfin.org/f-support)
+--- Forum: General Questions (https://forum.jellyfin.org/f-general-questions)
+--- Thread: Nginx reverse proxy (/t-nginx-reverse-proxy--10512)



Nginx reverse proxy - SjoerdHekking - 2025-02-02

I got a server on 192.168.2.10, that has nginx and functions as a reverse proxy. My jellyfin server at 192.168.2.92 works correctly in browser when typing it in directly.

Code:
server {
    listen 80;
    listen [::]:80;
    server_name test.something.com.com;

    # Uncomment to redirect HTTP to HTTPS
    return 301 https://$host$request_uri;
}

server {
    # Nginx versions prior to 1.25
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    # Nginx versions 1.25+
    #listen 443 ssl;
    #listen [::]:443 ssl;
    #http2 on;

    server_name test.something.com.com;

    ## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc.
    client_max_body_size 20M;

    # Comment next line to allow TLSv1.0 and TLSv1.1 if you have very old clients
    ssl_protocols TLSv1.3 TLSv1.2;

    ssl_certificate /etc/letsencrypt/live/test.something.com.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/test.something.com.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/test.something.com.com/chain.pem;

    # use a variable to store the upstream proxy
    set $jellyfin 192.168.2.92;

    # Security / XSS Mitigation Headers
    add_header X-Content-Type-Options "nosniff";

    # Permissions policy. May cause issues with some clients
    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
    # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
    # Enforces https content and restricts JS/CSS to origin
    # External Javascript (such as cast_sender.js for Chromecast) 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'";

    location / {
        # Proxy main Jellyfin traffic
        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-Protocol $scheme;
        proxy_set_header X-Forwarded-Host $http_host;

        # Disable buffering when the nginx proxy gets very resource heavy upon streaming
        proxy_buffering off;
    }

    location /socket {
        # Proxy Jellyfin Websockets traffic
        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-Protocol $scheme;
        proxy_set_header X-Forwarded-Host $http_host;
    }
}

This is my nginx conf file, and the SSL certificates were generated correctly.

Sadly, when going to the domain, it says "error too many forwards", ufw is set to allow 8096 on both the reverse proxy and the jellyfin server.

What have I done wrongly?


RE: Nginx reverse proxy - TheDreadPirate - 2025-02-03

You don't need port 8096 open on the reverse proxy. Just the Jellyfin server. Port 80 and 443 needs to be open on the reverse proxy.

On your router, port 443 is forwarded to the reverse proxy?

Can you share a censored version of /var/log/nginx/access.log via privatebin.net?


RE: Nginx reverse proxy - SjoerdHekking - 2025-02-03

(2025-02-03, 12:53 AM)TheDreadPirate Wrote: You don't need port 8096 open on the reverse proxy.  Just the Jellyfin server.  Port 80 and 443 needs to be open on the reverse proxy.

On your router, port 443 is forwarded to the reverse proxy?

Can you share a censored version of /var/log/nginx/access.log via privatebin.net?

Before I reply, I just want to say, that you're a legend for always helping people! Gosh, if I see that cute little duck, I know it's gonna be you.

I put in the external="" parameter, and everything is/was fixed... very odd, lol...


RE: Nginx reverse proxy - TheDreadPirate - 2025-02-03

Are you referring to the Published URI field?


RE: Nginx reverse proxy - SjoerdHekking - 2025-02-03

(2025-02-03, 12:52 PM)TheDreadPirate Wrote: Are you referring to the Published URI field?

Yes


RE: Nginx reverse proxy - visualblind - 2025-02-07

(2025-02-03, 10:14 AM)SjoerdHekking Wrote:
(2025-02-03, 12:53 AM)TheDreadPirate Wrote: You don't need port 8096 open on the reverse proxy.  Just the Jellyfin server.  Port 80 and 443 needs to be open on the reverse proxy.

On your router, port 443 is forwarded to the reverse proxy?

Can you share a censored version of /var/log/nginx/access.log via privatebin.net?

Before I reply, I just want to say, that you're a legend for always helping people! Gosh, if I see that cute little duck, I know it's gonna be you.

I put in the external="" parameter, and everything is/was fixed... very odd, lol...

I know you got it working already, but there might be something worth using from my own config:
https://is.gd/Zm8RwC


Code:
# Proxy Cache
proxy_cache_path /dev/shm/nginx-cache/jellyfin levels=1:2 keys_zone=jellyfin:20m max_size=1600M inactive=52h use_temp_path=off loader_threshold=400 loader_files=325;

# Limit Connections Zone
limit_conn_zone $binary_remote_addr zone=perip:10m;

upstream jellyfin_backend {
    server X.X.X.X:8096;
    keepalive 32;
    keepalive_requests 1000;
    keepalive_timeout 180s;
}

server {
    ### Redirect to site root ###
    listen 80;
    listen [::]:80;
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ~^(www\.)?travisflix\.com$;

    include proxy_params;
    include moz_ssl;

    return 301 https://travisflix.com$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name travisflix.com cdn.travisflix.com;

    include proxy_params;
    include moz_ssl;
    set $jellyfin_server "http://jellyfin_backend";

    real_ip_header X-Forwarded-For;

    brotli on;
    brotli_static on;
    include brotli.types;

    access_log /var/log/nginx/travisflix.com.cache.log cache;
    error_log /var/log/nginx/travisflix.com.error.log warn;

    root /var/www/travisflix.com;
    proxy_set_header Range $http_range;
    proxy_set_header If-Range $http_if_range;
    proxy_buffering on;

    # Security / XSS Mitigation Headers
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    # HSTS HTTPS Security
    add_header Strict-Transport-Security "max-age=2592000";

    # Content Security Policy
    # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
    # Enforces https content and restricts JS/CSS to origin
    # External Javascript (such as cast_sender.js for Chromecast) must be whitelisted.

    add_header Content-Security-Policy "default-src 'self' blob: data: https: https://i.ibb.co https://image.tmdb.org; connect-src 'self'; font-src 'self' data: https://fonts.gstatic.com/ https://fonts.googleapis.com; img-src 'self' https://repo.jellyfin.org https://raw.githubusercontent.com https://cdn.travisflix.com https://i.ibb.co https://image.tmdb.org https://assets.fanart.tv http://assets.fanart.tv https://m.media-amazon.com/ https://static.tvmaze.com/; object-src 'none'; script-src 'self' 'unsafe-inline' blob: https://www.gstatic.com/ https://www.youtube.com https://static.cloudflareinsights.com/beacon.min.js https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js https://cdn.jsdelivr.net; script-src-elem 'self' 'unsafe-inline' youtube.com blob: https://www.gstatic.com/ https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https://cdnjs.cloudflare.com/";

    if (-f $document_root/maintenance.html) {
        return 503;
    }

    error_page 404 @http404;
    error_page 429 @http429;
    error_page 503 @maintenance;

    location @maintenance {
        rewrite ^(.*)$ /maintenance.html break;
    }

    location @http404 {
        internal;
        root /var/ErrorPages/404colors;
        rewrite ^(.*)$ /404.html break;
    }

    location @http429 {
        rewrite ^(.*)$ /http429.html break;
    }

    location = / {
        return 302 https://$host/web/;
    }

    location / {
        include proxy_params;
        proxy_connect_timeout 30s;
        add_header X-Nginx-Forwarded-Host $http_host;
        add_header Strict-Transport-Security "max-age=2592000";
        proxy_pass $jellyfin_server;
    }

    location /socket {
        # Proxy Jellyfin Websockets traffic
        proxy_pass $jellyfin_server;
        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;
    }

    ### Cache images ###
    location ~ ^/Items/(.*)/Images|/web/.*(?:\.js|\.css|\.woff2) {
        include proxy_params;
        sendfile on;
        proxy_pass $jellyfin_server;
        proxy_cache_key "$host$request_uri";
        proxy_cache jellyfin;
        proxy_cache_revalidate on;
        proxy_cache_lock off;
        proxy_cache_valid 200 301 302 48h;
        proxy_cache_methods GET HEAD;
        proxy_cache_min_uses 1;
        proxy_ignore_headers Expires Cache-Control Set-Cookie X-Accel-Expires Vary;
        proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
        add_header X-Nginx-Cache $upstream_cache_status; # HTTP response header "X-Nginx-Cache" = HIT/MISS/BYPASS/EXPIRED
    }

    location ~ ^/Items/(.*)/Download$ {
        include proxy_params;

        ## Rate-limit all downloads
        limit_rate 8192k; # Speed in Kilobyte/s
        limit_conn perip 3; # Simultaneous connections/per ip
        limit_conn_status 429;
        proxy_buffering on; # Required for limit_conn

        sendfile off;
        add_header X-Nginx-Forwarded-Host $http_host;
        add_header Strict-Transport-Security "max-age=2592000";
        proxy_pass $jellyfin_server;
    }

    location /-_- {
        include proxy_params;
        default_type "video/mp4";
        alias /var/www/zoidberg/futurama-zoidberg.mp4;
    }

    location /help/ {
        proxy_set_header Host help.travisflix.com;
        add_header X-Nginx-ClientIP $remote_addr;
        add_header X-Nginx-Proxy $proxy_host;
        add_header X-Nginx-Forwarded-Host $http_host;
        add_header Strict-Transport-Security "max-age=2592000";
        proxy_pass https://visualblind.github.io/;
    }

    location /web/assets/img/icon-transparent.png {
        sendfile on;
        rewrite ^(.*)$ /web/img/icon-transparent-tf.png break;
    }

    location ~ /web(/assets)?/img/banner-(light|dark)\.png$ {
        sendfile on;
        rewrite ^(.*)$ /web/img/banner-travisflix.png break;
    }

    location /web/img/ {
        sendfile on;
        try_files $uri $uri/ =404;
    }

    location /web/test/ {

        if ($request_uri ~* "([^/]*$)" ) {
            set  $last_path_component  $1;
        }

        return 301 https://$host/test/$last_path_component;
    }

    location ~* ^/Videos/(.*)$ {
        include proxy_params;
        tcp_nodelay on;
        tcp_nopush on;
        sendfile off;
        add_header X-Nginx-Stream "tcp_nodelay=ON tcp_nopush=ON sendfile=OFF";
        add_header X-Nginx-Forwarded-Host $http_host;
        add_header X-Nginx-Proxy $proxy_host;

        # Disable buffering when the nginx proxy gets very resource heavy upon streaming
        proxy_buffering off;
        proxy_pass $jellyfin_server;
    }

    location ~ ^/Playback/(BitrateTest.*)$ {
        include proxy_params;
        proxy_buffering off;
        add_header X-Nginx-Proxy $proxy_host;
        proxy_pass $jellyfin_server;
    }

    location /Sessions/Capabilities/ {
        proxy_buffering off;
        proxy_request_buffering off;
        proxy_pass $jellyfin_server;
    }

    location ~ ^/(web/)?favicon.(.*) {
        sendfile on;
        try_files /favicon.$2 /web/favicon.$2 /web/img/favicon.$2 =404;
        add_header X-Nginx-ClientIP $remote_addr;
    }

    location /web/bc8d51405ec040305a87.ico {
        rewrite ^(.*)$ /favicon.ico break;
    }

    location ~ ^/web/(379bab68d056910336f9|3f3fe0fd3a0b637b5030|f5bbb798cb2c65908633|23a72f5d56f82554aeab|d6ecf2254db85ff3b545|522fa270807b7b12a9ba|a962662957ebbb8eb436|baafa93a783b76e667ec|0df719b48efcaef953df|106a7abc109fb5e78742|49d14d0eb7bcdf6f2d1b|f94ebf203ea0c91a47c6|d0e56683308a17dba86d|0b37f660ac0f7f01ab41|6de874568a98308c4a74|d31413d3f03c0873ccbb|16fc81178d1aee54f6cc|e62987a12a58b24f383a|39209dd2362c0db7c673|eb8bef4f19b6ad227f46|142d834c201895a46a01|bbb3e6d43389ba0d436c|7b8ef9809145cfec0aa6|3fa90c593184d5737eb3|d28a57b1e61f9f0dabd9|6a2e2e6b4186720e5d4f)\.png$ {
        rewrite ^(.*)$ /favicon.png break;
    }

    location /web/baba78f2a106d9baee83.png {
        rewrite ^(.*)$ /web/img/logo/tf-logo-536.png break;
    }

    location ~ ^/([0-9a-z]+)\.txt {
        sendfile on;
        include proxy_params;
        more_set_headers 'Content-Type: text/plain';
        try_files $1.txt $uri =404;
        add_header X-Nginx-ClientIP $remote_addr;
        add_header X-Nginx-Txt $1;
    }

    location ~ ^/(web/)?touchicon(72|114|144|512)?\.png$ {
        sendfile on;
        try_files /web/img/touchicon/touchicon$2.png /web/touchicon$2.png =404;
        add_header X-Nginx-ClientIP $remote_addr;
    }

    location /netdata/ {
        sendfile on;
        try_files $uri $uri/ =404;
    }

    location /loaderio-548b6134a9005e7b66f4b8ddabf08733 {
        try_files /loaderio-548b6134a9005e7b66f4b8ddabf08733.txt $uri =404;
    }

}