Nginx as a Reverse Proxy: Complete Configuration for Self-Hosted Services
When I first started self-hosting multiple services—Nextcloud, Jellyfin, Gitea, Vaultwarden—I quickly realized I couldn't just run them all on different ports and expect my users to memorize 192.168.1.100:8000 and 192.168.1.100:8001. I needed a reverse proxy, and after comparing Caddy, Traefik, and Nginx Proxy Manager, I chose Nginx for its raw flexibility and performance. This guide walks through real, production-tested configurations I'm running on my homelab right now.
Why Nginx Over Other Reverse Proxies?
I picked Nginx because it's fast, uses less RAM than the Docker-based alternatives, and handles certificate renewal without magic—you see exactly what's happening. Caddy is excellent if you want automatic HTTPS with minimal config, but Nginx gives me finer control over caching, compression, and upstream health checks. For a 2–3 service homelab, Caddy wins; for 5+ services with specific tuning needs, Nginx scales cleanly.
Nginx runs as a single process on the host (not in Docker, though that's an option). My typical setup is Ubuntu 22.04 LTS on a VPS or a Proxmox VM, with Nginx listening on ports 80 and 443, forwarding traffic to Docker containers running on 127.0.0.1:8000, 127.0.0.1:8001, etc.
Installation and Basic Setup
I install Nginx and Certbot from Ubuntu repos:
sudo apt update && sudo apt install -y nginx certbot python3-certbot-nginx curl
sudo systemctl enable nginx
sudo systemctl start nginx
Check it's running:
sudo systemctl status nginx
curl http://localhost
You'll see the default Nginx welcome page. Now I disable the default site and create my own config.
sudo nginx -t. A typo can take your entire homelab offline.Core Reverse Proxy Configuration
I structure my Nginx configs under /etc/nginx/sites-available/, with symlinks in /etc/nginx/sites-enabled/. Here's a complete, real-world config for a homelab running Nextcloud, Jellyfin, and Vaultwarden:
sudo nano /etc/nginx/sites-available/homelab
# Redirect HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name nextcloud.example.com jellyfin.example.com vault.example.com;
# Let Certbot validate during renewal
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Redirect everything else to HTTPS
location / {
return 301 https://$server_name$request_uri;
}
}
# Nextcloud upstream
upstream nextcloud_backend {
server 127.0.0.1:8080;
keepalive 32;
}
# Jellyfin upstream
upstream jellyfin_backend {
server 127.0.0.1:8096;
keepalive 32;
}
# Vaultwarden upstream
upstream vaultwarden_backend {
server 127.0.0.1:80;
keepalive 32;
}
# Nextcloud HTTPS
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name nextcloud.example.com;
# SSL certificates from Certbot
ssl_certificate /etc/letsencrypt/live/nextcloud.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/nextcloud.example.com/privkey.pem;
# Strong SSL config
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-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Nextcloud requires some specific headers
client_max_body_size 512M;
location / {
proxy_pass http://nextcloud_backend;
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;
# Nextcloud needs this
proxy_redirect off;
proxy_buffering off;
}
}
# Jellyfin HTTPS
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name jellyfin.example.com;
ssl_certificate /etc/letsencrypt/live/jellyfin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/jellyfin.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
# Jellyfin can handle large uploads
client_max_body_size 100M;
location / {
proxy_pass http://jellyfin_backend;
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_buffering off;
}
}
# Vaultwarden HTTPS
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name vault.example.com;
ssl_certificate /etc/letsencrypt/live/vault.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
proxy_pass http://vaultwarden_backend;
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;
}
# Vaultwarden WebSocket
location /notifications/hub {
proxy_pass http://vaultwarden_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
Enable the site and test:
sudo ln -s /etc/nginx/sites-available/homelab /etc/nginx/sites-enabled/homelab
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
Obtaining SSL Certificates with Certbot
I use Let's Encrypt via Certbot for free, auto-renewing certificates. The Nginx plugin handles everything:
sudo certbot certonly --nginx \
-d nextcloud.example.com \
-d jellyfin.example.com \
-d vault.example.com \
--agree-tos \
--email [email protected]
Certbot automatically renews 30 days before expiry. Verify:
sudo certbot renew --dry-run
The certificates live in /etc/letsencrypt/live/, and Nginx reloads them automatically after renewal thanks to the certbot systemd timer.
certbot certonly --dns-cloudflare and install the Cloudflare plugin first.Advanced Features: Caching and Compression
For serving static assets (images, CSS, JS), I enable Gzip compression and proxy caching. Add this to the http block in /etc/nginx/nginx.conf:
# Global compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss
application/rss+xml application/atom+xml image/svg+xml;
# Proxy cache for static content (e.g., Jellyfin thumbnails)
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=cache_zone:10m max_size=1g inactive=60d;
proxy_cache_key "$scheme$request_method$host$request_uri";
Then in your Jellyfin server block, add caching:
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp)$ {
proxy_pass http://jellyfin_backend;
proxy_cache cache_zone;
proxy_cache_valid 200 7d;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header Cache-Control "public, max-age=604800";
add_header X-Cache-Status $upstream_cache_status;
}
Monitoring and Debugging
I monitor Nginx logs to catch misconfigurations and upstream issues:
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log
For debugging a specific service, I enable debug logging temporarily:
sudo nginx -s reload -c /etc/nginx/nginx.conf
And watch real-time requests:
sudo watch -n 1 'wc -l /var/log/nginx/access.log'
Production Checklist
Before going live with your reverse proxy, I ensure:
- All upstreams are running and responding on localhost (test with
curl http://127.0.0.1:8080) - SSL certificates are valid:
sudo certbot certificates - Nginx config passes validation:
sudo nginx -t - Firewall allows 80 and 443:
sudo ufw allow 80/tcp && sudo ufw allow 443/tcp - DNS records point to your server IP
- Test each domain from a browser before telling users
For VPS hosting, I recommend RackNerd KVM VPS—affordable, reliable, and perfect for running Nginx + Docker services.
Next Steps
From here, you can add rate limiting to prevent brute-force attacks, implement basic authentication for development services, or set up load balancing across multiple upstream servers. The foundation is solid, and Nginx's flexibility means you can grow your homelab without replacing the reverse proxy.
Keep monitoring those logs, and don't skip the security headers—they're as important as the reverse proxy itself.