How to Configure NGINX Reverse Proxy for Secure Home Lab Access

I set up my first home lab with half a dozen self-hosted services — Jellyfin, Home Assistant, Nextcloud, a dashboard — all running on different ports. For a while I just memorized them all. 192.168.1.100:8096 for media, 192.168.1.100:8123 for home automation, and so on. It worked, but it was ugly, hard to share, and impossible to secure properly. The moment I started thinking about accessing any of it remotely, the port chaos became a real problem. That’s when I sat down and finally configured NGINX as a proper reverse proxy — and it completely changed how my home lab works. If you’re in the same situation, this guide walks you through the whole setup, start to finish.


What Is a Reverse Proxy and Why Does Your Home Lab Need One?

A reverse proxy sits in front of all your services. Instead of clients connecting directly to 192.168.1.100:8096, they connect to something like jellyfin.yourdomain.com — and NGINX quietly routes that request to the right service behind the scenes.

For a home lab, this matters for several reasons:

  • One IP, one port (443) — no more memorizing different ports for every service
  • SSL termination — NGINX handles HTTPS for all services, even if the apps themselves don’t support it
  • Security layer — NGINX can block bad requests, rate-limit brute-force attempts, and hide your internal network layout
  • Clean URLsnextcloud.yourdomain.com looks a lot better than 192.168.1.100:8080
  • Easier firewall rules — you only need one port open externally instead of many

What You Need Before You Start

Before touching any config files, make sure you have the following in place:

  • A Linux server or VM running Ubuntu 22.04 or 24.04 (other distros work but this guide uses Ubuntu)
  • NGINX installed on that server
  • A domain name you control (free options like DuckDNS work for home labs)
  • DNS pointing your domain or subdomain to your home’s public IP
  • Port 80 and 443 forwarded to your NGINX server in your router
  • At least one self-hosted service running internally (even a basic web app on a local port is fine)

Step 1: Install NGINX

If you haven’t already, install NGINX on your Ubuntu server.

bash

sudo apt update && sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx

Check that it’s running:

bash

sudo systemctl status nginx

You should see active (running) in the output. You can also open your server’s IP in a browser and you’ll see the default NGINX welcome page.


Step 2: Install Certbot for Free SSL Certificates

You need SSL certificates to serve traffic over HTTPS. Let’s Encrypt provides free certificates, and Certbot handles everything automatically — including renewals.

bash

sudo apt install certbot python3-certbot-nginx -y

Now request a certificate for your domain. Replace yourdomain.com with your actual domain or subdomain:

bash

sudo certbot --nginx -d yourdomain.com

If you’re using multiple subdomains, add them all in one command:

bash

sudo certbot --nginx -d jellyfin.yourdomain.com -d nextcloud.yourdomain.com -d homeassistant.yourdomain.com

Certbot will ask a few questions, verify domain ownership via HTTP, and install the certificates automatically. When it asks about HTTP-to-HTTPS redirect, choose yes.

Test that automatic renewal works:

bash

sudo certbot renew --dry-run

If you see Congratulations, all simulated renewals succeeded — you’re good. Certbot sets up a cron job that renews certificates automatically before they expire.


Step 3: Create Your First Reverse Proxy Config

NGINX config files for individual sites live in /etc/nginx/sites-available/. You create a file there and then symlink it to /etc/nginx/sites-enabled/ to activate it.

Here’s a clean, production-ready reverse proxy config for a service running on port 8096 (Jellyfin in this example):

nginx

# /etc/nginx/sites-available/jellyfin.yourdomain.com

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name jellyfin.yourdomain.com;
    return 301 https://$host$request_uri;
}

# Main HTTPS server block
server {
    listen 443 ssl http2;
    server_name jellyfin.yourdomain.com;

    # SSL certificates (managed by Certbot)
    ssl_certificate /etc/letsencrypt/live/jellyfin.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/jellyfin.yourdomain.com/privkey.pem;

    # Modern TLS settings
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Proxy settings
    location / {
        proxy_pass http://192.168.1.100:8096;
        proxy_http_version 1.1;
        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;

        # WebSocket support (needed by many services)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

Enable the site and reload NGINX:

bash

sudo ln -s /etc/nginx/sites-available/jellyfin.yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

The nginx -t command tests your config for syntax errors before reloading. Always run it before reloading — a syntax error in a config file can take down all your sites.


Step 4: Add Multiple Services with Separate Config Files

The cleanest way to manage multiple home lab services is to give each one its own config file. Repeat the process from Step 3 for each service, just changing the domain name and the proxy_pass address.

For example, for Nextcloud running on port 8080:

nginx

# /etc/nginx/sites-available/nextcloud.yourdomain.com

server {
    listen 80;
    server_name nextcloud.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name nextcloud.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/nextcloud.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/nextcloud.yourdomain.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Increase upload size for file uploads
    client_max_body_size 1G;

    location / {
        proxy_pass http://192.168.1.100:8080;
        proxy_http_version 1.1;
        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;
    }
}

Enable it and reload:

bash

sudo ln -s /etc/nginx/sites-available/nextcloud.yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Keep one file per service — it makes troubleshooting much easier when something breaks.


Step 5: Add Rate Limiting to Stop Brute-Force Attacks

Without rate limiting, anyone can hammer your login pages with thousands of password attempts. NGINX has built-in rate limiting that’s easy to configure.

Open your main NGINX config:

bash

sudo nano /etc/nginx/nginx.conf

Inside the http {} block, add this line:

nginx

limit_req_zone $binary_remote_addr zone=homelab:10m rate=10r/s;

This creates a zone called homelab that limits each IP to 10 requests per second, using a 10MB memory zone to track addresses.

Then, in each of your site config files, add this inside the location / block:

nginx

limit_req zone=homelab burst=20 nodelay;

This allows short bursts of up to 20 requests (for normal browsing) but blocks anything that looks like a bot or brute-force attempt.

Reload NGINX after saving:

bash

sudo nginx -t && sudo systemctl reload nginx

Step 6: Hide NGINX Version Info

By default, NGINX includes its version number in error pages and HTTP headers. That’s free information for anyone probing your server for known vulnerabilities.

Turn it off in /etc/nginx/nginx.conf inside the http {} block:

nginx

server_tokens off;

Reload NGINX, then verify it’s hidden:

bash

curl -I https://yourdomain.com

The Server: header should now show nginx without a version number.


Step 7: Restrict Admin Pages to Local IPs Only

Some of your home lab services have admin panels that should never be accessible from outside your home network. You can lock those paths down by IP directly in NGINX.

For example, to allow only local network access to an admin path:

nginx

location /admin {
    allow 192.168.1.0/24;
    deny all;
    proxy_pass http://192.168.1.100:8080/admin;
}

Anyone hitting /admin from outside your local subnet gets a 403 Forbidden response — no password prompt, no way in.


Step 8: Set Up a Shared SSL Snippet (Optional But Clean)

If you have many services, you’ll repeat your SSL and security header lines in every config file. A cleaner approach is to put them in a shared snippet and include it everywhere.

Create the snippet:

bash

sudo nano /etc/nginx/snippets/ssl-security.conf

Add your common settings:

nginx

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Then in any site config, replace those repeated lines with a single include:

nginx

include /etc/nginx/snippets/ssl-security.conf;

This keeps your configs short and consistent — change the security settings in one place and they apply everywhere instantly.


Step 9: Test Everything and Monitor Logs

After setting up your reverse proxy, always verify that things are actually working as expected.

Check that HTTPS redirects are happening correctly:

bash

curl -I http://jellyfin.yourdomain.com

You should see a 301 Moved Permanently redirect to HTTPS.

Check your security headers are being sent:

bash

curl -I https://jellyfin.yourdomain.com

Look for Strict-Transport-Security, X-Content-Type-Options, and X-Frame-Options in the output.

Monitor your NGINX access log in real time to watch traffic and spot anything suspicious:

bash

sudo tail -f /var/log/nginx/access.log

And watch the error log for any proxy issues:

bash

sudo tail -f /var/log/nginx/error.log

Common Issues and Fixes

502 Bad Gateway — NGINX can’t reach the backend service. Check that the service is actually running on the IP and port you specified in proxy_pass. Also confirm there’s no firewall blocking connections between NGINX and the backend.

SSL certificate errors — Make sure your domain’s DNS is pointing to the right IP and that port 80 is open (Certbot needs it for verification). Run sudo certbot certificates to see what certificates you have and when they expire.

WebSocket not working — Some services like Home Assistant and certain dashboards need WebSocket support. Make sure your config includes the Upgrade and Connection headers shown in Step 3.

Config won’t reload — Always run sudo nginx -t before reloading. It will show you exactly which line has the syntax error.

Large file uploads failing — Add client_max_body_size 1G; (or whatever size you need) inside the relevant server block. The default is only 1MB.


FAQ

Do I need a public domain name for a home lab reverse proxy?
Not necessarily. For local-only access, you can use local DNS with something like Pi-hole or a custom /etc/hosts entry and a self-signed certificate. But for remote access or valid SSL certificates from Let’s Encrypt, you do need a real domain. Free options like DuckDNS work perfectly fine.

What’s the difference between NGINX and Nginx Proxy Manager?
Nginx Proxy Manager (NPM) is a web UI built on top of NGINX. It makes adding proxy hosts and SSL certificates much easier for beginners — no config files needed. The trade-off is slightly more overhead and less fine-grained control. The raw NGINX approach in this guide gives you full control and better performance.

Can I run NGINX inside Docker for my home lab?
Yes, and many home labbers prefer it. Running NGINX in a Docker container via Docker Compose makes it easier to manage alongside your other containerized services. The config files work identically — you just mount them into the container.

Is it safe to expose my home lab services to the internet through NGINX?
With proper SSL, security headers, rate limiting, and keeping software updated — yes, it’s a significant improvement over direct port exposure. That said, avoid exposing anything you consider high-risk (like a local admin panel) without extra protection like IP whitelisting or a VPN.

What ports do I need to open on my router?
Just port 80 (for HTTP and Let’s Encrypt certificate verification) and port 443 (for HTTPS). Both should forward to your NGINX server’s local IP. You don’t need to open any other ports for your individual services.

How do I add a new service after the initial setup?
Create a new config file in /etc/nginx/sites-available/, run sudo certbot to get a certificate for the new subdomain, symlink the config to sites-enabled, test with nginx -t, and reload. It takes about 5 minutes once you’ve done it a couple of times.

What if my home IP address changes?
Most home internet connections use dynamic IPs. Set up a Dynamic DNS (DDNS) service — DuckDNS, Cloudflare, or your router may have a built-in option — to automatically update your DNS record when your IP changes.


Editor’s Opinion

honestly setting up nginx felt kinda scary the first time, like all those config files and ssl stuff seemed really complicated. but once i actually sat down and did it, its not that bad at all. the thing that helped me most was just doing one service at a time and not trying to configure everything at once. i made the mistake of trying to do 5 subdomains in one go and when somthing broke i had no idea where to look lol. also the nginx -t command is your best friend — run it every single time before you reload, seriously. now my whole homelab is behind nginx and it feels way more “real” like an actual setup instead of just a bunch of ports i have to remember. if you been putting it off just do it, its worth it

Leave a Comment