DEV Community

Thiago David
Thiago David

Posted on

How to Securely Expose Your Jellyfin Using Oracle Cloud (Free Tier), Tailscale, and Nginx

If you have a Jellyfin media server at home, one of the biggest challenges is sharing access with friends or reaching your content while you're away. Opening ports on your home router is often impractical due to CGNAT, and it also exposes your home network to unnecessary risks.

In this guide, we'll set up an extremely secure and efficient architecture using only free tools:

  1. Tailscale: Creates a secure private virtual network (Mesh VPN) between your home and the cloud.
  2. Oracle Cloud (Free Tier): A free VPS that will serve as our public gateway to the internet.
  3. Nginx: Will act as a Reverse Proxy on the VPS, managing SSL (HTTPS) certificates and forwarding streaming traffic in an optimized way.

Paid requirements:

  1. Domain: You'll need to own a domain that you can use to expose Jellyfin access to the internet.

1. Tailscale Configuration

Tailscale will be responsible for invisibly connecting our Oracle VPS to your Jellyfin server at home.

  1. Create an account at Tailscale.
  2. Download and install the Tailscale client on the computer or server where your Jellyfin is running.
  3. Follow the setup wizard to authenticate the device.
  4. Note down the private IP address provided by Tailscale for your Jellyfin server (it will look something like 100.x.y.z).

⚠️ Important: just like we'll do later for the VPS, go to the Tailscale dashboard (login.tailscale.com), find this machine (your home Jellyfin server), and enable Disable key expiration. If only the VPS has this enabled and the home server's key expires, the connection drops on both ends anyway — the home server falls "off the network" even though the VPS is fine.

Step 1.1: Prepare the Jellyfin Host Firewall to Accept Tailscale Connections

Nginx will talk to Jellyfin through the Tailscale private IP (100.x.y.z:8096). If the home machine has a local firewall active, it can block that connection even though Tailscale itself is working fine — and this usually fails silently (timeout, no clear error).

If the Jellyfin server is on Linux (with ufw):

sudo ufw allow in on tailscale0
Enter fullscreen mode Exit fullscreen mode

This allows all traffic arriving through the Tailscale interface, without needing to expose port 8096 to the local network or the internet.

If the Jellyfin server is on Windows:
In Windows Defender Firewall, create an Inbound Rule allowing TCP port 8096, restricting the Remote IP address to the Tailscale range (100.64.0.0/10). Alternatively, check that the "Tailscale" network adapter is classified as Private (not Public) in Windows network settings — this usually allows local traffic automatically for that profile.


2. Oracle Cloud Infrastructure (OCI) Configuration

Oracle offers excellent free instances on its Always Free Tier. Let's create our network infrastructure and virtual machine.

Step 2.1: Create the VCN (Virtual Cloud Network)

  1. In the Oracle dashboard, go to Networking -> Virtual Cloud Networks.
  2. Click Start VCN Wizard, select the default option with internet connectivity.
  3. Keep the default settings, give it an easy-to-remember name, and complete the creation.

VCN Configuration

Step 2.2: Configure Ingress Rules (Cloud Firewall)

By default, Oracle blocks all external traffic. We need to allow access on the web and SSH ports.

  1. Inside your VCN, go to Security Lists and click Default Security List for .
  2. Click Add Ingress Rules and add the following two rules:

Rule 1: SSH Access

  • Source CIDR: 0.0.0.0/0
  • IP Protocol: TCP
  • Destination Port Range: 22

Rule 2: Web Traffic (HTTP/HTTPS)

  • Click Add Ingress Rules again.
  • Source CIDR: 0.0.0.0/0
  • IP Protocol: TCP
  • Destination Port Range: 80,443

Step 2.3: Create the Instance (VPS)

  1. Go to Compute -> Instances -> Create Instance.
  2. Select the Ubuntu operating system (latest LTS version) and choose the Always Free Eligible shape.
  3. In the Networking section, select the VCN you just created.
  4. Important: Expand the Advanced Options section at the bottom of the page and, in the Management / Cloud-init script field, paste the script below.

Why this script? Oracle's Ubuntu images come with very strict native iptables rules that block web traffic even after we open the ports in the dashboard. This script safely clears those rules at boot time.

Cloud-init script injection

#!/bin/bash
# Safely removes Oracle's native firewall at boot
iptables -F
iptables -X
iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT
netfilter-persistent save
systemctl stop netfilter-persistent
systemctl disable netfilter-persistent
Enter fullscreen mode Exit fullscreen mode
  1. Add your public SSH key so you can access the machine later, then click Create.
  2. Wait until the instance switches to Running state and copy its Public IP Address.

3. DNS Configuration (Cloudflare)

For the SSL certificate to work correctly, you need your own domain pointing to your VPS.

  1. Go to your DNS provider's dashboard (such as Cloudflare).
  2. Create an A record (e.g., jelly.yourdomain.com) pointing to the public IP of the Oracle instance.

⚠️ Important (if using Cloudflare): leave the proxy disabled on this record (grey cloud — "DNS only", not the orange cloud). With Cloudflare's proxy enabled, Nginx sees Cloudflare's IP instead of the visitor's real IP, and video streaming can suffer from the free plan's size/timeout limits. Keep it on DNS only so the connection goes straight to your VPS.


4. VPS Configuration and Hardening

Now let's access the VPS via SSH to install Nginx, Tailscale, and Certbot. Replace ORACLE_IP with your virtual machine's public IP:

ssh ubuntu@ORACLE_IP
Enter fullscreen mode Exit fullscreen mode

Step 4.1: Install and Configure UFW on the VPS

Instead of leaving the host firewall completely wide open (relying only on Oracle's Security List), let's install UFW and configure specific rules — this adds an extra layer of protection in case the Oracle network configuration changes in the future.

sudo apt install ufw -y

# Default policy: block incoming, allow outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH, HTTP and HTTPS
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Allow all traffic coming through the Tailscale interface
sudo ufw allow in on tailscale0

# Enable the firewall
sudo ufw enable
Enter fullscreen mode Exit fullscreen mode

Note: install Tailscale (Step 4.3) before running ufw enable, otherwise the tailscale0 interface won't exist yet — in that case, just run sudo ufw allow in on tailscale0 again after installing Tailscale.

Step 4.2: Update the System and Install Fail2Ban

Make sure the server is up to date and protected against SSH brute-force attacks:

# Update repositories and packages
sudo apt update && sudo apt upgrade -y

# Install Fail2Ban for basic protection against SSH attacks
sudo apt install fail2ban -y
Enter fullscreen mode Exit fullscreen mode

Step 4.3: Install and Authenticate Tailscale on the VPS

Now, let's bring the Oracle VPS into the same private network as your Jellyfin server at home:

# Automated Tailscale installation
curl -fsSL https://tailscale.com/install.sh | sh

# Authenticate the VPS to your Tailscale account
sudo tailscale up
Enter fullscreen mode Exit fullscreen mode

Click the link generated in the terminal to authorize the new machine in the Tailscale dashboard.

⚠️ Very Important: Go to the Tailscale web dashboard (login.tailscale.com), find the machine corresponding to your Oracle VPS, click the three dots next to it, and select Disable key expiration. This prevents the connection from automatically dropping every 60 days.

If you installed UFW before Tailscale, run this now so the tailscale0 interface rule actually applies:

sudo ufw allow in on tailscale0
Enter fullscreen mode Exit fullscreen mode

Step 4.4: Install Nginx and Certbot

Install the web server and the tool that automates the creation of our free Let's Encrypt SSL certificate:

sudo apt install nginx certbot python3-certbot-nginx -y
Enter fullscreen mode Exit fullscreen mode

Step 4.5: Create the Initial Nginx Configuration (HTTP)

Let's create a temporary HTTP (port 80) configuration file just to let Certbot validate our domain.

sudo nano /etc/nginx/sites-available/jellyfin
Enter fullscreen mode Exit fullscreen mode

Paste the following content, replacing YOUR_DOMAIN with your actual domain (e.g., jelly.yourdomain.com):

server {
    listen 80;
    listen [::]:80;
    server_name YOUR_DOMAIN;

    location / {
        return 301 https://$host$request_uri;
    }
}
Enter fullscreen mode Exit fullscreen mode

Enable the configuration and restart Nginx:

# Create the symbolic link to enable the site
sudo ln -s /etc/nginx/sites-available/jellyfin /etc/nginx/sites-enabled/

# Remove the default Nginx configuration to avoid conflicts
sudo rm /etc/nginx/sites-enabled/default

# Test the syntax and restart the service
sudo nginx -t
sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

Step 4.6: Generate the SSL Certificate with Certbot

Run the command below to automatically generate the HTTPS certificate:

sudo certbot --nginx -d YOUR_DOMAIN
Enter fullscreen mode Exit fullscreen mode

Follow the on-screen instructions, enter a valid email, and accept the terms of service.

Step 4.7: Final Optimized Reverse Proxy Configuration

With the certificate generated, let's replace the Nginx configuration file content to include Jellyfin streaming optimizations, WebSocket support, and security headers.

Open the file again:

sudo nano /etc/nginx/sites-available/jellyfin
Enter fullscreen mode Exit fullscreen mode

Delete the old content and paste the full configuration below. Make sure to replace YOUR_DOMAIN with your domain and YOUR-JELLYFIN-SERVER-IP with the Tailscale private IP of your home server (noted in Step 1):

# BLOCK 1: HTTP (Port 80) redirect to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name YOUR_DOMAIN;

    return 301 https://$host$request_uri;
}

# BLOCK 2: HTTPS Reverse Proxy (Port 443)
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name YOUR_DOMAIN;

    # SSL CONFIGURATION (Automatically generated by Certbot)
    ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Essential Security Headers
    add_header X-Content-Type-Options "nosniff";

    # Access and Error Logs
    access_log /var/log/nginx/jellyfin.access.log;
    error_log /var/log/nginx/jellyfin.error.log;

    # Buffer and Timeout Optimization for Heavy Content
    proxy_connect_timeout 4s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;

    proxy_buffers 8 1024k;
    proxy_buffer_size 1024k;
    proxy_busy_buffers_size 2048k;
    client_max_body_size 100M;

    # Main route forwarded to the home Jellyfin server's Tailscale IP
    location / {
        proxy_pass http://YOUR-JELLYFIN-SERVER-IP: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;

        # CRUCIAL: Disabling proxy_buffering ensures video streaming
        # starts immediately without stutters caused by Nginx's buffer.
        proxy_buffering off;
    }

    # Dedicated route for Jellyfin WebSockets (real-time sync)
    location /socket {
        proxy_pass http://YOUR-JELLYFIN-SERVER-IP: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;

        # Extended timeout to prevent the persistent WebSocket connection from dropping
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}
Enter fullscreen mode Exit fullscreen mode

Apply the changes and reload the service:

sudo nginx -t
sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

Conclusion

All set! From now on, when accessing https://your-domain.com, the connection will securely hit your Oracle VPS, pass through the encrypted Tailscale tunnel, and deliver the content directly from your home server with a valid SSL certificate and no open ports on your home router.

Top comments (0)