Self-hosting: Publishing privately to Friends & Family

Article 3 of 4 on publishing an App to a private network for friends & family
Self-hosting: Publishing privately to Friends & Family

This article (part 3) will walk-through how to set-up an application that you want to allow your friends and family to access over the internet without exposing it to the wider public internet. In this case, the application to share is 'Jellyfin', think of it like Netflix or Plex but it is Open Sourced.

The guide introduces Tailscale, an application that creates device to device private connectivity similar to a VPN, to remove the need to expose any services unnecessarily whilst applying 'Access Control Lists' to ensure people you invite only access devices / ports you want to share.

Scenario: I want to host and share an app from my home to my friends and family

Below are some questions I asked myself before venturing on this journey:

  • What app/service am I hosting? A service to watch videos
  • Why am I hosting it? Cause I want to share my videos / data with my friends securely
  • Who is/are the user(s)? Me & my friends. We want to watch entertainment from anywhere!
  • Where will the user(s) access it? My friends should be able to access it from the internet, any device, anywhere, anytime
  • Is it worth hosting this myself? Yes, cause subscriptions suck

Difficulty, Tech Stack & Requirements

  • Difficulty: Novice to intermediate Enthusiast. You will need to understand your way around a terminal. Know basics of ssh and terminal text-based editors and how to troubleshoot yourself
  • Time Required: 60 minutes
  • Tech Stack / Requirements:
    • A computer with a internet connection and a network
      • Internal DNS - E.g. Local router or DNS server and admin access to update it
    • A computer host that can run Ubuntu Server OS - it will be our Docker proxy host that is preferably separate from your Jellyfin host
    • An External DNS domain name (Just use CloudFlare DNS, approx. $20AUD a year)
    • Apps:
      • Docker, a container virtualisation software, this should already be installed (free)
      • Jellyfin, a free and open source application that is like Plex or Netflix, should already installed
      • NGINX Proxy Manager, a free and open source GUI-based reverse proxy used to provide SSL certs and serve sites over 443, covered in article 1.
      • New: Tailscale, a popular Device to Device based "VPN" (free)
      • Cloudflared DNS, ensures people outside your local network go via the Tailscale Tailnet (paid) and covered in article 2.
Disclaimer 1: This article is recommended for enthusiasts that are willing to dive a little deeper, due to the nature of Jellyfin there are rabbit holes for 'volumes vs bind mounts for storage of media', enabling 'hardware acceleration for encoding' and other tangents which is dependent on the hardware you have for your system. You must be willing to RTFM and use Google-fu (or an AI these days) to overcome the obstacles.
Disclaimer 2: This article aims to provide new users a simple way to share services whilst teaching basics like network segmentation, network ACLs and DNS. There are definitely more robust and customisable ways to achieve the end outcome, this is merely one.

Technical Steps

This article / guide is broken up into 3 parts. It is assumed you already have access to a Ubuntu Server, with Docker & Jellyfin installed and you're now ready to share Jellyfin with your friends and family.

This guide walks through the green boxes and lines
  • Part 1: Deploy NGINX Manager as a Container
  • Part 2: Jellyfin Network & Reverse Proxy Configs
  • Part 3: Deploy Tailscale as a Container with Tailscale ACLs
If you don't have Jellyfin, please go here to deploy it via Docker: https://jellyfin.org/docs/general/installation/container/

Part 1: Deploy NGINX Manager as a Container

Similar to Article 1, we're going to deploy NPM (Nginx Proxy Manager), NPM is a reverse proxy that is easy to use and is configurable enough for our use-case.

  1. Open up your local Terminal (command prompt.exe / terminal.app) and ssh into your Ubuntu External Reverse Proxy Server, install docker unless you have already done so.
  2. Create a new directory using the command cd ~/ && mkdir ~/nginx-proxy-manager && cd ~/nginx-proxy-manager
  3. Create a new docker compose file with nano compose.yaml
    1. Copy the contents below, and paste it in
    2. Hit ctrl+o to write to file, then ctrl+x to quit nano
services:
  app:
    image: 'docker.io/jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'
      - '81:81'
      - '443:443'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
  1. Use cat compose.yaml to validate the contents of the file we just made. It should look like the above
  2. Use docker compose up -d to start the containers up. Give it a minute and then in your browser, open up one of the following:
    1. If you set-up NPM locally on your Mac/Windows/Linux, go to http://localhost:81/ in your web browser
    2. If you set-up NPM on another system, go to that IP address with :81 appended. Example: http://192.168.1.1:81/
  3. You should now see the start-page of Nginx Proxy Manager. Using the following default credentials to sign in and set-up a new account.
    1. User: admin
    2. Pass: changeme

You've set-up the base of NGINX Proxy Manager, lets configure NPM & Jellyfin for Reverse Proxy connections.

Part 2: Jellyfin Network & Reverse Proxy Configs

The following configurations only apply to Jellyfin but the principles apply to other apps you want to deploy in this scenario.

  1. Sign into your Jellyfin instance with an account with admin access
  2. Navigate to "Dashboard > Advanced > Networking"
  3. Locate LAN networks and Known proxies
  4. Enter your respective information for your local networks into LAN Networks and your Nginx Proxy Manager IP addresses
    1. LAN Networks: Add the subnet mask, if you're not sure what that is, add a /8 to your local network address
    2. Known Proxies: Add the IP address of your NPM server's IP, here I also have the Tailscale IP which we will get later in this guide
Jellyfin's Networking configs
  1. Sign into your Nginx Proxy Manager console (e.g. 192.168.1.123:81)
  2. Select 'Add Host'
  3. Enter in the local network details of your Jellyfin host under details:
    1. Domain Names: jellyfin.domain.com (use the domain you have set-up)
    2. Scheme: http
    3. Forward hostname / IP: 192.168.1.123 (*jellyfin local IP address)
    4. Forward Port: 8096
    5. Block Common Exploits: Checked
    6. Websockets Support: Checked
Add Proxy Host > Details (Use your respective details)
  1. Select the 'SSL' tab and edit the following:
You will need an SSL certificate if you don't want SSL errors
  1. Select the Settings Cog in the top right hand corner of the Add Proxy Host window and then add the following nginx config information.
Make sure to change the internalip to your internal ip address of your Jellyfin Server
    location / {
        # Proxy main Jellyfin traffic
        proxy_pass http://internalip: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 #treaming
        proxy_buffering off;
    }

location /embywebsocket {
        # Proxy Jellyfin Websockets traffic
        proxy_pass http://internalip: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;
   }

The NGINX code block for jellyfin to serve traffic over websockets

  1. Select 'Save' to save the Proxy Host configuration
  2. You can now test it by going to the new URL, it should load the Jellyfin page without any issues!
🛑 Caution: Local DNS record recommended 🛑
It is recommended to add a local DNS entries to ensure you route locally. E.g. a CNAME & A Record for Jellyfin and your Reverse Proxy respectively. Otherwise you will either go directly to the Jellyfin IP:Port or route via Tailscale all the time
Go to Part 3 in this guide: https://blog.timothyduong.me/self-hosting-partone-publishing-locally/ otherwise, please carry on

Part 3: Deploying Tailscale as a Container with ACLs

Here we will be deploying Tailscale as a Container, why a container? So I can migrate the container & configuration between machines if I need to stand it up. If I install Tailscale binary, it would enable the Tailnet onto my Host machine (e.g. ssh access) but I don't want that and only want to share the HTTP/HTTPS network access to this box.

In my deployment, I also have a separate Ubuntu server set-up and it is sitting in my 'DMZ' network. I have it set-up like this as I'm sharing with other people access to my external reverse proxy rather than my internal reverse proxy or the services directly. That way, I can add additional controls, policies and segment the users easily.

  1. Login or Sign-up for Tailscale: https://login.tailscale.com/
  2. Once signed in, navigate to 'Tags': https://login.tailscale.com/admin/acls/visual/tags
  1. Select 'Create Tag' and fill out the tag details:
    • Tag name: shared or sharedservices
    • Tag Owner: autogroup:owner
    • Note: Add meaningful notes to help remind yourself later what it does
Create Tag detailed prompt window
  1. Select 'Save tag' then navigate to 'General Access Rules' and '+ Add Rule'
Tailscale's Access Controls - where we create tags and ACLs
  1. Fill out the rules as required and hit 'Save Grant'
    • Source: All users & devices (*)
    • Destination: tag:shared
    • Port & Protocol: 443 80
    • Note: ACL to allow invited users to access Devices tagged 'Shared' on port 80 & 443
Example of the ACL to only provide access to specific machines on specific ports
  1. Navigate to the Admin Portal and to 'Settings > Keys': https://login.tailscale.com/admin/settings/keys
Tailscale Admin Console > Settings > Keys > Auth Keys
  1. Select 'Generate auth key' and Enable the following settings:
    • Description: jellyfin-proxy-key (enter whatever you feel fits)
    • Reusable: No (Recommend it to be no)
    • Pre-approved: Yes
    • Tags: tag:shared
Creating a temporary new authkey for the tailscaled container
  1. Select 'Generate Key' and copy the whole key e.g. tskey-auth-the-rest-of-the-key
Demo key, I've since deleted the key
  1. SSH into your Proxy Server
  2. Create a new directory for Tailscale using cd ~/ && mkdir tailscale && cd tailscale
  3. Create the Tailscale docker compose.yaml nano compose.yaml
  4. Enter the following into the compose.yaml
services:
  tailscale:
    image: tailscale/tailscale
    hostname: jellyfin-tailscale
    cap_add:
      - NET_ADMIN
      - NET_RAW
    environment:
      - TS_AUTHKEY=tskey-auth-the-rest-of-the-key
      - TS_SOCKET=/var/run/tailscale/tailscaled.sock
      - TS_EXTRA_ARGS=--accept-routes
      - TS_STATE_DIR=/var/lib/tailscale
    volumes:
      - ./data:/var/lib # Creates a tailscale directory under /data for persistence
      - /dev/net/tun:/dev/net/tun
    network_mode: host
    restart: unless-stopped
networks: {}

replace 'tskey-auth-the-rest-of-the-key' with the real key you copied from Tailscale

  1. Run the following to spin up the container sudo docker compose pull && sudo docker compose up -d
  2. Check your Tailscale Admin Console > Machines
  1. Select the three dots '...' and select 'Disable Key Expiry' to remove the need to re-authenticate the server / container every 90 days, otherwise, users will lose access
This way we don't need to re-authenticate it every 3 months
  1. We will now need to add the Tailscale IP and associate it with your External DNS provider / Cloudflare DNS, this is optional but recommended. Locate the machine and under IP address to copy it to clipboard.
  1. Load up Cloudflare DNS and create a new DNS A record:
    • Record Name: ext-proxy
    • Type: A
    • Content: the tailscale IP address. e.g., 100.76.150.8
  1. Then create a new DNS CNAME Record:
    • Record Name: jellyfin
    • Type: CNAME
    • Content: ext-proxy.domain.com
Example of the 2 records for me
These 2 records will ensure that users will hit the private Tailscale IP address when accessing from the internet / publically. They will need to have tailscale enabled and also be invited to your tailnet
  1. Your network stack should be ready now, and all you need to do is share your Machine
  2. From Tailscale, locate the Machine, on the right hand side is the dots ..., select 'Share'
    1. Select 'Share via email' and add the email you'd like to share this machine with
  1. Once a person connects, you may need to manually approve them from the Tailscale Admin Console
What an end user sees if they need to be approved
  1. Once they're approved, they should be able to access your Jellyfin server if they have installed Tailscale and have it connected.

Phew, we're done! We can test it externally by turning off Wi-Fi from your mobile device, turning on Tailscale and attempting to access Jellyfin.

    • If we enable Tailscale & disable Wi-Fi, Jellyfin should load
    • If we disable Tailscale & Wi-Fi, Jellyfin should not load
    • If we enable Wi-Fi & Tailscale, Jellyfin should load

Optional Appendices

Tailscale - Enabling Auto-approve ✅

  • You can enable auto-approve in the Settings > User Management > User Approval:

Jellyfin & Tailscale on TV Devices 📺

Yes, you can have your private network accessible by yourself/others as long as their devices can install Tailscale.

Once installed alongside the Jellyfin TV apps, they can watch Jellyfin using the native players.

Conclusion

We walked through a way for end users to securely access your Jellyfin app privately over the internet without exposing it to other randoms (the public) and set-up the following:

  • Nginx Proxy Manager, to create a reverse proxy to serve the application(s) over HTTPS/443
  • Tailscale, to enable private access for selected users that you have invited
  • Tailscale ACLs, to ensure those invited users can only access what you let them access
  • External DNS records to allow people from outside your network to access via Tailscale

Questions, alternatives & improvements

  • Why not place Tailscale on the host directly (Binary)? I want to be able to back-up and restore my device easily without having to re-invite people to the machine.
  • Why not invite people to the Tailnet and not the machine?
  • Why not caddy / traefik? Caddy & traefik and even pangolin are way more robust and powerful tools, but they come with learning curves. If you know about those tools, the more power to you!
  • Why not PLEX/Emby instead of Jellyfin? I just like Jellyfin, you definitely can use Plex or Emby instead. This guide is mainly to teach the principals.
  • Why not netbird or headscale instead of Tailscale? Tailscale has popular auth 'SSO' providers enabled. If you know what that means, then this article is not for you!
  • What are some better alternatives?
    • Media Server / Jellyfin: Plex, Emby
    • Reverse Proxy / Nginx Proxy Manager: Nginx, NPM-plus, Pangolin, Traefik, Caddy
    • Private Network / Tailscale: Netbird, Pangolin, Tailscale+Headscale
    • External DNS / Cloudflare DNS: I prefer Cloudflare DNS cause there's less steps for Cloudflare Tunnel but Route53 works well too.

Final Part 4

The final article (Part 4) will use another tool called 'Wizarr' in a hybrid deployment that streamlines user-onboarding processes to Tailscale & Jellyfin and combines all the learnings from Articles 1, 2, & 3.

Other articles are: