Tuxvador's Blog
Automated Daily CrowdSec Reports via Email Managing DNS Programmatically with the IONOS API Getting Started with Proxmox VE for Your Home Lab My Proxmox VE Homelab: Infrastructure Overview Monitoring Your Homelab with Grafana, Loki, and CrowdSec

Setting Up a Reverse Proxy with NGINX for Self-Hosted Services


If you self-host multiple web services — a blog, a monitoring dashboard, a file server — you quickly face a problem: each service wants port 80 and 443. A reverse proxy solves this by sitting in front of all your services and routing requests based on domain names.

Architecture

The idea is simple:

Client → tuxvador.fr:443 → NGINX Reverse Proxy
                            ├── /blog → dev-srv:5000
                            ├── /grafana → mon-srv:8889
                            └── /api → dev-srv:3001

The reverse proxy is the only container exposed to the internet (via port forwarding on your router). All other services are internal, unreachable from outside, and have no public IP.

Installing NGINX

On Debian 13:

apt update && apt install -y nginx certbot python3-certbot-nginx

Configuring a Basic Proxy

Here’s a minimal server block for proxying to a blog running on port 5000:

server {
    listen 80;
    server_name tuxvador.fr;

    location / {
        proxy_pass http://10.99.99.20:5000;
        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;
    }
}

The key directives:

  • proxy_pass — where to forward requests. Use internal IPs, not public ones.
  • proxy_set_header — forward the original client info so the backend sees real IPs.
  • server_name — which domain this block handles.

Adding TLS with Certbot

Once your domain points to the proxy’s public IP:

certbot --nginx -d tuxvador.fr -d www.tuxvador.fr

Certbot automatically modifies your NGINX config to serve HTTPS, sets up redirects, and configures auto-renewal via a systemd timer.

Multi-Service Setup with Different Domains

If you have multiple domains pointing to the same IP:

# Blog
server {
    listen 443 ssl;
    server_name tuxvador.fr;
    ssl_certificate /etc/letsencrypt/live/tuxvador.fr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tuxvador.fr/privkey.pem;

    location / {
        proxy_pass http://10.99.99.20:5000;
        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;
    }
}

# Grafana
server {
    listen 443 ssl;
    server_name monitoring.tuxvador.fr;
    ssl_certificate /etc/letsencrypt/live/monitoring.tuxvador.fr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/monitoring.tuxvador.fr/privkey.pem;

    location / {
        proxy_pass http://10.99.99.10:8889;
        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;
    }
}

Sub-path vs Subdomain

You have two options for routing:

  • Subdomain: blog.tuxvador.fr → app A, monitoring.tuxvador.fr → app B
  • Sub-path: tuxvador.fr/blog → app A, tuxvador.fr/monitoring → app B

Subdomains are cleaner for apps that expect to be at root. Sub-paths require the backend app to support path-based routing (or you rewrite paths). I use subdomains for production services.

Security Headers

Add these to every HTTPS server block:

add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Verifying Your Setup

nginx -t                    # test config
systemctl reload nginx      # apply changes
certbot renew --dry-run     # verify renewal works

Common Pitfalls

WebSockets: Apps like Jupyter or VS Code Server need WebSocket support. Add to location block:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Large uploads: By default NGINX limits request body size to 1 MB. Increase with:

client_max_body_size 100M;

Logging: NGINX logs everything to /var/log/nginx/. Monitor these for unusual requests — they’re the first sign of scanners probing your services.