Post

Make Jellyfin at Home Accessible to the Internet with Tailscale and nginx

A basic guide for setting up a subdomain that points to an nginx reverse proxy, exposing your Linux box at home. All using Tailscale.

Make Jellyfin at Home Accessible to the Internet with Tailscale and nginx

Since you are here, you probably already know that Jellyfin is an free media server. Its great for streaming movies and shows. It also stores music and ebooks. It is a great alternative to Plex. You stream your entire library of public domain shows and movies, like Teenagers from Outer Space (1953) and The Gold Rush (1925). Whatever is in your library, you may want to make it available to your friends and family. This guide will show you how, using Tailscale to create your own close VPN.

Disclaimer: After getting this setup you will want to look in to hardening the server running Jellyfin.

A brief explanation of this setup

You could simply use Tailscale to connect from any device (e.g. a tablet) to your Jellyfin media server. This guide takes it a step further, making your Jellyfin media server available to the internet at your domain. At the end of this guide you will have two computers configured as servers. The servers will be…

  1. A server running Jellyfin, on your home network.
  2. A server running nginx, out on the internet (I used a VPS on Vultr).

The instructions below were tested on two computers running Ubuntu 24.04 server. You should be able to easily adapt these instructions for any other Linux distro since the configs should still work. The only difference is the configs could be located in different locations. Also, Tailscale and Jellyfin are easy to install on most distros.

Configure DNS

You will need a domain where you can manage DNS. In this guide we will point the subdomain jellyfin.example.com to your server running nginx. If you do not have a domain, you could probably try a dynamic DNS service.

Configure your domain by setting up an A record for your domain or subdomain (e.g. jellyfin.example.com) that points to the public IP address of the server running nginx. With a quick web search, you will find endless guides for doing this.

On the server running Jellyfin

This section of the instructions is for the computer that will be sitting on your home network.

Install required packages

If you need, take a look at Tailscale’s official documentation for installing tailscale for your system. Below are the instructions for Ubuntu.

Add Tailscale’s package signing key and repository.

1
2
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list

Install tailscale.

1
2
sudo apt update
sudo apt install tailscale

Connect to your Tailscale network. Follow the instructions in the command line to authenticate.

1
sudo tailscale up

Now install Jellyfin. They have made this extremely easy with their install script. Download it while, at the same time, checking the integrity of the script.

1
diff <( curl -s https://repo.jellyfin.org/install-debuntu.sh -o install-debuntu.sh; sha256sum install-debuntu.sh ) <( curl -s https://repo.jellyfin.org/install-debuntu.sh.sha256sum )

Run the script.

1
sudo bash install-debuntu.sh

Once the install script is done, there will be a URL where you can access Jellyfin. Go to that URL in your browser to create an admin account and configure a few options. After that you should now be able to log in to the dashboard. If you get the Jellyfin app on your phone or streaming device, your new Jellyfin server should be automatically detected.

You have a fully working Jellyfin media server! But it is not accesible to the internet. You still need to setup the server out on the internet.

On the server running nginx

This section of instructions is for the machine that is going to be sitting on the internet. This machine will not need much for resources. It will only be passing along network traffic. So if you are getting a VPS, you don’t need to spend much on this one. If you need a cheap VPS, check out Vultr for an easy way to quickly spin up and destroy a server. Crunchbit is another great option. Here are a couple of referral links that will help me out and one even gives you $100.

Install required packages

If you need, take a look at Tailscale’s official instructions on installing tailscale for your system. Below are the instructions for Ubuntu.

Add Tailscale’s package signing key and repository.

1
2
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list

Install tailscale.

1
2
sudo apt update
sudo apt install tailscale

Connect to your Tailscale network. Follow the instructions in the command line to authenticate.

1
sudo tailscale up

Install nginx.

1
sudo apt install nginx

Install certbot.

1
sudo snap install --classic certbot

Get an SSL/TLS certificate

Be sure ports 80 and 443 are open on the server, if they aren’t already. You can check this with ufw status. If you do not see 80/tcp or 443/tcp listed as ALLOWED then you will need to open the ports as shown below.

1
2
sudo ufw allow 80/tcp # Open port 80 from anywhere on the internet.
sudo ufw allow 443/tcp # Open port 80 from anywhere on the internet.

Now you can attempt to get a cert. There is a limited number of times per day that you can do this. So if you just want to test it, use the --dry-run flag. Also be sure to replace jellyfin.example.com with your actual domain or subdomain.

1
certbot certonly --nginx -d jellyfin.example.com

If you cannot execute certbot, go to certbot.eff.org where they have instructions for installing it on most systems. There may be another step, like creating a symlink to /usr/bin or something like that.

Configure nginx

Create an nginx vhost for the reverse proxy that will handle the traffic from this server and your Jellyfin media server. Create a file where the configs for the vhost will live.

1
2
sudo vi /etc/nginx/sites-available/jellyfin # Opens an new file in vi editor, saved at the path.
# sudo nano /etc/nginx/sites-available/jellyfin # Do it in nano if you want.

The nginx config below is taken from Jellyfin’s documentation. Copy the config below and paste it in to the editor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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 jellyfin.example.org;

    ## 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/example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.org/chain.pem;

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

    # 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;
    }
}

server {
    listen 80;
    listen [::]:80;
    server_name jellyfin.example.org;
    return 301 https://$host$request_uri;
}

Be sure to make these modifications to the config you just copied and pasted.

  • Replace any occurance of jellyfin.example.org with your domain/submdomain.
  • Replace 127.0.0.1 with the Tailscale IP address of your Jellyfin server, found in your Tailscale admin panel.
  • Replace /etc/letsencrypt/live/example.org/ with /etc/letsencrypt/live/yoursite.com/

Create a symlink to this file in the sites-enabled directory.

1
ln -s /etc/nginx/sites-available/jellyfin /etc/nginx/sites-enabled/

Test nginx to be sure it will restart without any problems.

1
sudo nginx -t

If the nginx configs will work, you will see a message stating ending in “test is successful”. Now restart nginx for the configs to take effect.

1
sudo systemctl reload nginx

If nginx restarted successfuly, you shouldn’t see any output. Try going to the domain you setup for accessing Jellyfin. It should load the Jellyfin login page from the server running on your home network. Now get to streaming!

One more thing: security!

Wait! There is one more important thing! You have just opened up your home server (the one running Jellyfin) to the entire internet. In other words, your entire home network could be exposed to the internet through the domain you set up. There are bots out there scanning 24/7 for servers to take over. So it is important that you do this.

Do some research on how to “harden an ubuntu server” or something to that effect. There are plenty of guides out ther on this. It should show you how to setup a firewall, failt2ban, and even limit ways to log in to the server. You could also look in to more precise firewall rules that only allow incoming traffic from IP ranges you want to allow to load the page. For example, only allow IP addresses of your family and friends.

Ok, with that said, now go have fun streaming!

This work by Jason Raveling is licensed under CC BY-ND 4.0 .