Windows with NGINX

This guide covers how to start from nothing to running a secure Jellyfin server on your computer at home.

It is recommend you be familiar or at least comfortable with advanced computer use.

Forced updates can lead to your system going offline while you are away from home. If you do not have the ability to remotely access, Jellyfin could be offline until you return home to correct this. Problems like blue screens or failed update loops are well documented on support threads all over the internet. A possible mitigation is to set up Active Hours to the time frame in which you are away from home. This way updates should be ran when you are home.

Installing Windows

Because Windows is, by a large margin, the most used OS for consumer computers, there is a 99% chance you already have this installed on a computer. Additionally, Windows is extremely easy to install. I won't cover this. You should instead refer to official documentation.

I highly recommend your Windows computer be connected via Ethernet. WiFi is not ideal.

Network Configuration

Login to your router. This is either 192.168.1.1 or something like routerlogin.net. You need to make a few changes. If you're unable to to make these changes, such as using shared complex internet, you can't continue as this step is required.

  1. Find DHCP reservations. Assign your server computer a static IP for the local network. You'll do this with the MAC address. Instructions vary by router, so read the manual. It's the same basic method. You assign a MAC address like 00:AA:BB:CC:DD to an LAN IP like 192.168.1.50. To find the MAC in Windows, click the Network icon in your taskbar, then click the Properties text on the currently connected network. In this window it is known as "Physical Address (MAC)".

  2. Find Port Forwarding. Forward ports 80 and 443 to the static IP you just set. You do not need or want to forward Jellyfins default ports. Only the two mentioned here.

  3. Optional: While you're at it you can change your DNS to OpenDNS, Cloudflare DNS, or Google DNS so all devices benefit from it.

Installing Software

The software you acquire comes in two forms - Archives or Installers. Some development teams do not bother to write installers for their software as it they deem it unnecessary. To make Jellyfin accessible outside of your home network you configure a reverse proxy. There are a handful of choices available for you to pick from. They all do the same job to reach the same end result, how they get there varies. I personally use NGINX because I am used to it and it's more configurable (like Apache). That's what this guide will use. A common suggestion on reddit is to use Docker due to it's ease of use and simplicity.

> nginx

Download a zip archive of NGINX: https://nginx.org/download/nginx-1.18.0.zip

Extract the contents of the archive to the root of your C drive. (EX: C:\nginx). NGINX will not run since there is no installer or configurable service. This will be covered later.

> jellyfin

Download an installer of Jellyfin: https://repo.jellyfin.org/releases/server/windows/stable/installer/jellyfin_10.6.4_windows-x64.exe

Run the installer. Select "Basic Install". You can optionally change where you want to store the server files and database. Jellyfin will not run once installed. You must open the start menu to launch the Jellyfin Tray App. You can access it at http://localhost:8096. Go through the initial setup process by adding a user. Do not create a user named "admin" as this unnecessary. Do not show users on the login screen. Do not allow unlimited login attempts (-1), this should be set to three (3). Don't worry about adding any media. You can do this later. Right click the Jellyfiin icon in the taskbar tray to make sure Autostart is checked.

> nssm

Download a zip archive of NSSM: https://nssm.cc/release/nssm-2.24.zip

nssm is a service helper which doesn't suck.

This makes running NGINX on startup a breeze. You configure the service once and forget it. At least until you want to uninstall it. Which is just as easy. Extract the contents of the archive to the root of your C drive. (EX: C:\nssm). You don't need to do anything further.

> certbot

This is optional. If you have no intentions of using your own TLD, skip installing.

Download an installer of Certbot: https://dl.eff.org/certbot-beta-installer-win32.exe

Certbot will create an SSL certificate for you using the service Lets Encrypt. You don't need to do anything further here.

> openssl

This is optional. If you have no intentions of using your own TLD, skip installing.

Download a zip archive of OpenSLL: https://kb.firedaemon.com/support/solutions/articles/4000121705

OpenSSL can create a Diffie-Hellman key (dhparam.pem) for you. Extract the contents of the archive to the root of your C drive. (EX: C:\openssl). You don't' need to do anything further.

Media Location

Where is your media stored? Shared LAN drive? Or connected internal HDD? Both you and Jellyfin are going to need to be able to access this root location as you acquire new media. At least so you don't have to physically use this computer. I have a basic WD MyCloud (no apps version) NAS that houses all my media. It is a shared LAN drive. It needs to mount every time Windows boots. Essentially permanent mount. If you're using an internal HDD, you're going need to share it's root folder over LAN. Follow the steps below for the method you're using.

LAN Drive (NAS)
Internal HDD
LAN Drive (NAS)
  1. Open File Explorer

  2. From the ribbon menu at the top, under Computer, select Map network drive.

  3. Select a letter mount point you want to use.

  4. The folder should be the IP and share: \\192.168.1.100\<shareName>

  5. Make sure Reconnect at sign-in is ticked.

  6. If your network drive is password protected, connect using different credentials should be ticked. Enter your username and password for this drive.

  7. Click Finish.

Internal HDD
  1. Open File Explorer.

  2. From the left pane select the disk.

  3. Right click on empty and click "Properties"

  4. Click the "Sharing" tab.

  5. Click "Advanced Sharing"

  6. Tick "Share this folder"

  7. Give it a name, add a comment and adjust permissions.

  8. Click OK.

On your other Windows computers you can access it at \\192.168.1.xx\<shareName>

NGINX Conf Files

Nginx uses .conf files to function. You're going to be editing an existing one and creating a new one.

You can use VSCode, Notepad++ or good 'ol Notepad to edit these files. Open C:\nginx\conf\nginx.conf in your text editor. Here's what mine looks like:

####
# Basic variable to run NGINX as a service
####
worker_processes 1;
events {
####
# Manages NGINX connections
####
worker_connections 1024;
multi_accept off;
}
http {
####
# Include basic configuration files
####
include mime.types;
#include fastcgi.conf;
include jellyfin.conf;
####
# Remove server version from pages and header
####
server_tokens off;
####
# Undefined mime types are downloaded instead of rendered
####
default_type application/octet-stream;
####
# These are for load management
####
#sendfile on;
#tcp_nopush on;
tcp_nodelay on;
proxy_buffering off;
client_max_body_size 50M;
####
# Compress contents
# Files that match the mime types that are larger than 100kb are compressed
# You may change gzip_comp_level
# 1 is low compression using low CPU
# 9 is high compression using high CPU
####
gzip on;
gzip_vary on;
gzip_min_length 100;
gzip_comp_level 4;
gzip_buffers 16 8k;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/json application/x-javascript application/javascript application/xml application/xml+rss application/xhtml+xml application/x-font-ttf application/x-font-opentype application/vnd.ms-fontobject font/woff font/woff2 font/otf font/ttf image/gif image/jpeg image/png image/x-ms-bmp image/svg+xml image/webp image/tiff;
gzip_disable "MSIE [1-6]\.";
####
# Set expirary headers to keep contents in cache
# This reduces server load and speeds up browsing.
####
map $sent_http_content_type $expires {
default off;
text/html epoch;
text/css max;
application/javascript max;
font/woff max;
font/woff2 max;
font/otf max;
font/ttf max;
~image/ max;
}
}

Right click on empty white space at C:\nginx\conf and click New > Text Document. Change the name and extension to jellyfin.conf and open it in a text editor. For reference, here is what mine looks like:

server {
####
# This is the HTTP server block.
# It should be used to redirect to HTTPS
####
listen 80;
listen [::]:80;
server_name jlyfn.example.com;
return 301 https://$host$request_uri;
}
server {
####
# This is the HTTPS server block
# It should be what enables Jellyfin
####
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name jlyfin.example.com;
#resolver "example.ddns.com";
#set $jellyfin 127.0.0.1;
####
# SSL section to secure your server domain.
# Manual configration is required
####
#ssl_certificate "C:\certbot\live\jlyfn.example.com\fullchain.pem";
#ssl_certificate_key "C:\certbot\live\jlyfn.example.com\privkey.pem";
#ssl_trusted_certificate "C:\certbot\live\jlyfn.example.com\chain.pem";
#ssl_dhparam "C:\certbot\live\dhparam.pem";
add_header Strict-Transport-Security "max-age=31536000" always;
#ssl_stapling on;
#ssl_stapling_verify on;
#ssl_session_cache shared:le_nginx_SSL:10m;
#ssl_session_timeout 1440m;
#ssl_session_tickets off;
#ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
#ssl_prefer_server_ciphers on;
#ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS";
####
# Security / XSS Mitigation Headers for old browsers. Newer browsers may ignore
####
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
####
# Connect to expiary headers to cache contents
####
expires $expires;
####
# Content Security Policy for modern browsers
# 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:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com/cv/js/sender/v1/cast_sender.js blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'";
location = / {
return 302 https://$host/web/;
}
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;
}
location = /web/ {
####
# This is purely for aesthetics so /web/#!/ works instead of having to go to /web/index.html/#!/
# Similar to a rewrite rule
####
proxy_pass http://$jellyfin:8096/web/index.html;
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;
}
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;
}
}

DNS

Let's talk about that. Specifically, these lines:

#resolver jlyfn.example.com;
#set $jellyfin 127.0.0.1;

Hopefully you have already acquired and setup a DDNS service. And/or optionally a TLD.

Manual DNS
Automatic DNS Purchased Domain
Automatic DNS Free Domain
Manual DNS

This is a DNS record managed by you. If you purchased your own domain from a provider like GoDaddy, you can use this option. If your dynamic home address changes, you need to update your record manually by logging into the domain sellers website. If you happen to have a static IP purchased from your ISP (in most cases business packages) this is the ideal method. Most people would recommend you don't do it this way. Home IPs are known to be ever revolving in certain areas. If your ISP is IPv6 only or prefers it over IPv4, you're in luck as these are highly unlikely to change.

With a purchased domain you can point a subdomain to your home IP address. To get your home IP you can search "what is my IP?" and copy it. Create a new A (IPv4) or AAAA (IPv6) record for your domain. It would look like one of these:

Type

Name

Value

TTL

A

jlyfn

50.100.50.100

1 hour

AAAA

jlyfn

FEDC:BA98:7654:3210:FEDC:BA98:7654:3210

1 hour

Do not use A and AAAA records at the same time. Use one or the other.

The "name" will be the subdomain where you are going to access your server (jlyfn.example.com) and the "value" is your home IP. Using a subdomain allows certain features of Jellyfin to work as expected. Using Jellyfin at a base URL in "sub directory" format can break features. You can read more at the link below.

In your conf file uncomment this line:

set $jellyfin 127.0.0.1;

You don't need the resolver line since you're pointing your domain name to your IP. Leave it commented out. Then change server_name to your new domain:

server_name jlyfn.example.com;

I use this method. My home IP hasn't changed since I've been here. And it's close to five years. Other people might not have this same experience. I use IPv6.

Automatic DNS Purchased Domain

This is a DNS managed for you. If your dynamic home address changes often, a DDNS service has you covered. With a purchased a domain you can still leverage a DDNS service. This is a recommend method for home users.

With your DDNS service and purchased domain you can point a subdomain to your DDNS URL. Create a new CNAME record for your domain. It would look something like this:

Type

Name

Value

TTL

CNAME

jlyfn

example.duckdns.org

1 hour

The "name" will be the subdomain where you are going to access your server (jlyfn.example.com) and the "value" is your DDNS URL. Using a subdomain allows certain features of Jellyfin to work as expected. Using Jellyfin at a base URL in "sub directory" format can break features. You can read more at the link below.

A running background task will keep your IP updated without user interaction. You should donate to the service if it helps you!

In your conf file uncomment these lines and edit the resolver line to your DDNS URL:

resolver "example.duckdns.org";
set $jellyfin 127.0.0.1;

Then change server_name to your new domain:

server_name jlyfn.example.com;

Your DDNS URL can also be used to access your server. This means jlyfn.example.com will lead to your Jellyfin service, and example.duckdns.org can show a "welcome to nginx" page. Of course your browser will also show a HTTPS error saying the certificate doesn't match. If you do not want the DDNS URL to show a page, you can block the request.

Create a new conf file at C:\nginx\conf (EX: block.conf) and paste in the following contents:

server {
listen 80;
listen [::]:80;
server_name _;
return 444;
}

Then edit your nginx.conf file and add block.conf to your includes:

http {
####
# Include basic configuration files
####
include mime.types;
include block.conf; <---- add this bit here
#include fastcgi.conf;
include jellyfin.conf;
[the rest of your conf file is below...]

A DNS cache flush may be required to process this if you visited your DDNS URL already: ipconfig /flushdns

Automatic DNS Free Domain

If you intend to run multiple services or websites you cannot use this method. You cannot assign the same URL to the server_name for different services or websites.

This is a DNS managed for you. If your dynamic home address changes often, a DDNS service has you covered. You don't need to purchase a domain if you choose this method. This is a recommend method for home users.

With your DDNS service you can use this a direct method to access your server. For example if you signed up with Duck DNS, you would go to example.duckdns.org to access your media server.

Using a subdomain allows certain features of Jellyfin to work as expected. Using Jellyfin at a base URL in "sub directory" format can break features. You can read more at the link below.

A running background task will keep your IP updated without user interaction. You should donate to the service if it helps you!

In your conf file uncomment this line:

set $jellyfin 127.0.0.1;

You don't need the resolver line since you're pointing your DDNS domain to your IP. Leave it commented out. Then change server_name to your DDNS domain:

server_name example.ddns.com;

SSL

If you are using your own purchased TLD, please use SSL. If you are using a DDNS domain, you do not have to complete this section.

While this is technically optional, I highly discourage using unencrypted remote access! Passwords and any API keys that may be in use are exposed. It's like walking into a store naked.

You need to get a cert. You have one option:

Once you have obtained your cert you also need to generate a Diffie-Hellman key (dhparam.pem). This buffs up protection against attackers. Making it exponentially harder for an attacker to decipher server to client communication. Complete the following:

  1. Go to C:\openssl\x64\bin in File Explorer and SHIFT right click on an empty space.

  2. Select "Open PowerShell window here".

  3. Generate your pem file.

    • ./openssl dhparam -dsaparam -out C:\certbot\live\dhparam.pem 4096

  4. Uncomment the ssl_dhparam line.

Firewall

Jellyfin remote connections are being handled through reverse proxy. There is no need to have the WAN IP allow remote connections. Jellyfin can disable this. To do that head to http://localhost:8096/web/index.html#!/networking.html and uncheck "Allow remote connections to this Jellyfin Server." Restart the server from the dashboard and verify from an externally connected device. Easiest way to do that is turn off your phones WiFi and visit your home IP in a web browser (Exmaple: http://50.100.50.100:8096.) If you see a Forbidden message the port is allowing connections, but Jellyfin is stopping it. However, you shouldn't even be able to connect to see this message. Windows Defender Firewall comes with three presets. Block all, Block unless rule found, and Allow. The default behavior of the firewall is to allow defined rules only. There isn't anything you need to do here. Allowing remote connections through the WAN IP is insecure and bypasses SSL. Because IPv6 violates HTTP URL formatting rules, you can't access an IPv6 IP directly from your address bar.

As a last resort, you use can NGINX to close the connection (444). You can add this to your jellyfin.conf file under the server block for 443 (SSL):

# This closes the connection if someone uses your WAN IP
if ($host != "sub.example.com") {
return 444;
}

This piece isn't necessary by any means if you see Jellyfin or ufw is working as intended. If the connection is allowed at the WAN IP, you can use this. if statements are a last resort here because Jellyfin and the firewall are capable of stopping requests.

NSSM

Unfortunately, NGINX can't be run as a service natively. You can double click the EXE to start the process, but you would have to do this every time you reboot. To get around this, NSSM comes in.

  1. Go to C:\nssm\win64 in File Explorer and SHIFT right click on an empty space.

  2. Select "Open PowerShell window here".

  3. Type ./nssm install NGINX and accept the admin prompt.

  4. The NSSM GUI will appear.

  5. Click the ... button beside the Path box.

  6. Locate nginx.exe and click Open.

  7. Click Install Service.

That's it. NGINX has now been installed as a system service and will automatically run anytime you reboot/boot.

Hardware Acceleration

On Windows, hardware accelerated encoding, decoding and transcoding is well supported. You should have no issues getting this feature to work. Whether you use Intel's IGPU, or a DGPU from AMD or Nvidia

Nvidia purposely impose a limit on their consumer grade (gaming) GPUs. NVENC/NVDEC by limitation of the driver is capped at a set amount. This varies based on GPU model, but the limit is often three (3). If you want to see what your GPU is capable of, look over the matrix for "Max concurrent sessions". They do this so you will purchase a business grade (commercial/working) GPU known as Quadro. Luckily the Linux community is ahead of this. They provide altercation scripts that remove this so you can use your GPU beyond this artificial limit. There is a video demonstration using a tool that is two years old that leverages the alternate Windows scripts of the same Linux project. AMD does not impose limits on consumer GPUs. While using Windows, I have seen my RX580 run 6 transcode processes before hitting a wall and using CPU. From my observations, the IGPU with QSV is capable of running 4 transcodes before it hits a limit and starts a CPU process.

Binge

You're done. You can add media and start watching. You should reboot your computer to get NGINX running. Your server should be accessible from your custom domain. If you encounter any errors you can ask for help on reddit. Don't go around the web publicly posting your server URL. Be selective with who you share it with.

Contributions

Something wrong? Want to add info? Help out! Let's discuss it.