Nginx Reverse Proxy Setup: Securing Internal Services with SSL/TLS
We earn commissions when you shop through the links on this page, at no additional cost to you.
I've been running self-hosted services in my homelab for three years now, and the single biggest security leap came when I moved from exposing services directly to using Nginx as a reverse proxy with proper SSL/TLS termination. It's the difference between having your Nextcloud instance yelling at you in red warnings and actually sleeping at night knowing your services are encrypted and protected from snooping.
In this guide, I'm walking you through the exact setup I use: Nginx handling SSL termination for multiple internal services, automatic certificate renewal, and the gotchas I've learned the hard way.
Why Nginx Reverse Proxy + SSL Matters
When you expose a service directly to the internet—say, Jellyfin on port 8096—your traffic travels unencrypted. Anyone between your client and server (ISPs, routers, networks) can sniff credentials. A reverse proxy solves this by sitting between clients and internal services, handling encryption at the edge.
Nginx specifically is lightweight (unlike Traefik), easy to reason about, and doesn't require Docker knowledge to configure. I prefer it because I can SSH into my box and edit a single config file without rebuilding containers.
The flow looks like this:
Client (HTTPS) → Nginx (port 443 + SSL cert) → Internal Service (port 8096)
(encryption terminates here) (plain HTTP is fine)
Installation and Base Configuration
I'll assume Ubuntu 22.04 LTS, which is my standard for self-hosted VPS work. If you need a cheap VPS to run this on, RackNerd's KVM VPS plans start at very reasonable prices and I've had solid uptime with them.
Install Nginx and Certbot:
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx ufw -y
sudo systemctl start nginx
sudo systemctl enable nginx
# Create a directory for your SSL configs
sudo mkdir -p /etc/nginx/sites-available
sudo mkdir -p /etc/nginx/sites-enabled
Obtain an SSL Certificate with Let's Encrypt
I use Certbot to manage Let's Encrypt certificates. The DNS-01 challenge works best for internal domains since you control DNS:
# For a domain you own (e.g., homelab.example.com)
sudo certbot certonly --dns-cloudflare \
-d homelab.example.com \
-d "*.homelab.example.com"
If using Cloudflare DNS, install the Cloudflare plugin first:
sudo apt install python3-certbot-dns-cloudflare -y
Certbot stores certificates at /etc/letsencrypt/live/homelab.example.com/. I'll reference these paths in the Nginx config.
sudo systemctl status certbot.timer. If renewal fails silently, your services will go down when the cert expires. I learned this the expensive way.Nginx Reverse Proxy Config
Here's my template config for proxying multiple services. Create a new file:
sudo nano /etc/nginx/sites-available/homelab.example.com
Paste this configuration:
# HTTP redirect to HTTPS (enforce encryption)
server {
listen 80;
listen [::]:80;
server_name homelab.example.com *.homelab.example.com;
return 301 https://$server_name$request_uri;
}
# HTTPS upstream (the actual reverse proxy)
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name homelab.example.com *.homelab.example.com;
# SSL certificate paths
ssl_certificate /etc/letsencrypt/live/homelab.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/homelab.example.com/privkey.pem;
# SSL security settings (A+ rating on SSL Labs)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Nextcloud (runs on localhost:8080)
location /nextcloud {
proxy_pass http://127.0.0.1:8080;
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;
# Nextcloud needs special handling
proxy_redirect off;
client_max_body_size 512M;
}
# Jellyfin (runs on localhost:8096)
location /jellyfin {
proxy_pass http://127.0.0.1:8096/jellyfin;
proxy_buffering off;
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;
}
# Vaultwarden (password manager on localhost:80)
location /vault {
proxy_pass http://127.0.0.1:8000;
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;
}
# Default 404
location / {
return 404;
}
}
Enable the config:
sudo ln -s /etc/nginx/sites-available/homelab.example.com \
/etc/nginx/sites-enabled/homelab.example.com
# Test the config for syntax errors
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
Firewall Configuration
Open only ports 80 (HTTP redirect) and 443 (HTTPS) to the internet. Internal services remain on private ports behind Nginx:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # SSH (your own IP if possible)
sudo ufw allow 80/tcp # HTTP (Let's Encrypt validation)
sudo ufw allow 443/tcp # HTTPS (clients)
sudo ufw enable
sudo ufw allow from 192.168.1.100/32 to any port 22 to restrict SSH to a specific IP address. This is one of the easiest wins for hardening your box.Testing and Troubleshooting
Test your setup from the internet. I use my phone on 4G:
# Check cert validity
curl -I https://homelab.example.com/nextcloud
# Look for these headers in the response:
# HTTP/2 200
# Strict-Transport-Security: max-age=31536000
# X-Frame-Options: SAMEORIGIN
If you get a 502 Bad Gateway, the upstream service isn't responding. Check that Nextcloud/Jellyfin are actually running on the ports you configured:
netstat -tlnp | grep LISTEN
# Look for 127.0.0.1:8080, 127.0.0.1:8096, etc.
Check Nginx logs for proxy errors:
sudo tail -f /var/log/nginx/error.log
If Nextcloud complains about being behind a proxy, add this to config/config.php:
'trusted_proxies' => array('127.0.0.1'),
'overwritehost' => 'homelab.example.com',
'overwriteprotocol' => 'https',
Certificate Renewal and Monitoring
Certbot should auto-renew. Test the renewal process:
sudo certbot renew --dry-run
Set up monitoring (I recommend Uptime Kuma to alert me if renewal fails):
# Check renewal timer status
sudo systemctl status certbot.timer
# Manually renew if needed
sudo certbot renew --force-renewal
Advanced: Subdomain Routing
If you prefer subdomains instead of paths, create separate server blocks:
server {
listen 443 ssl http2;
server_name nextcloud.homelab.example.com;
ssl_certificate /etc/letsencrypt/live/homelab.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/homelab.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}
Use a wildcard cert certificate (as shown in the Certbot command) to cover all subdomains with a single cert.
Next Steps
Now that your services are encrypted at the edge, consider adding authentication in front of Nginx using Authelia or OAuth. This adds a login layer before traffic reaches your apps—perfect for exposing services to friends or family without giving them direct access to credentials.
Also, if you're running multiple self-hosted services, evaluate whether you need a VPS at all. Many people run the reverse proxy on a cheap RackNerd KVM VPS while keeping heavier services (Nextcloud, Jellyfin, Ollama) at home on a NAS or mini PC behind this proxy. This keeps your home IP private while still exposing services securely.
Discussion