Nginx Configuration - Reverse Proxy for the fogserv Fleet

Status: Active
Last Updated: 2026-08-26
Category: Networking - Phase 2: Services
Prerequisites: reverse-proxy-basics, dns-fundamentals, tls-configuration
Time: 2 hours
Tags: nginx, reverse-proxy, certbot, tls, gzip, homelab

Summary

Configure nginx as a reverse proxy for the fogserv fleet: server blocks per service, proxy_pass to backends, automatic TLS via certbot, and sensible compression and buffering defaults. This gives us a third reverse-proxy option alongside Caddy and Traefik for hosts where nginx is already installed or preferred.

๐ŸŽฏ What You'll Learn

By the end of this article, you'll be able to:


Table of Contents

  1. Context / Why This Matters
  2. Implementation / Core Content
  3. Practical Examples
  4. Troubleshooting & Common Pitfalls
  5. Next Steps / Ops Actions

Context / Why This Matters

The fleet standard is Cloudflare DNS in front of either Caddy (caddy-reverse-proxy) or Traefik v3 (traefik-v3-reverse-proxy). Nginx earns its place on hosts that already run it (e.g., package-installed web stacks) or where you want one battle-tested binary doing both static serving and proxying. The concepts are identical โ€” host-based routing to upstream services โ€” only the syntax differs.

Before starting, make sure your DNS records point at this host (see cloudflare-dns) and ports 80/443 are open in nftables (firewalls-nftables).

Implementation / Core Content

Install

sudo apt install nginx certbot python3-certbot-nginx

Layout convention on our hosts:

/etc/nginx/
โ”œโ”€โ”€ nginx.conf              # main config (keep distro defaults mostly)
โ”œโ”€โ”€ conf.d/                 # global snippets (gzip, proxy defaults)
โ””โ”€โ”€ sites-available/        # one file per virtual host
     โ””โ”€โ”€ sites-enabled/      # symlinks to enabled sites

Global tuning: /etc/nginx/conf.d/tuning.conf

# Proxy defaults inherited via include in server blocks
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;
proxy_connect_timeout 5s;
proxy_send_timeout    60s;
proxy_read_timeout    60s;

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_types text/plain text/css application/json application/javascript
           application/xml text/xml application/rss+xml image/svg+xml
           text/javascript application/wasm;

# Hide version banner
server_tokens off;

Server block pattern

/etc/nginx/sites-available/grafana.fogserv.cloud:

server {
    listen 80;
    listen [::]:80;
    server_name grafana.fogserv.cloud;

    # certbot manages the redirect; before issuance keep this serving ACME challenges
    location /.well-known/acme-challenge/ { root /var/www/html; }
    location / { return 301 https://$host$request_uri; }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name grafana.fogserv.cloud;

    ssl_certificate     /etc/letsencrypt/live/grafana.fogserv.cloud/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/grafana.fogserv.cloud/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # Real client IP comes through Cloudflare; restore it if needed with
    # ngx_http_realip_module + Cloudflare IP ranges.

    location / {
        proxy_pass http://127.0.0.1:3000;
        include /etc/nginx/conf.d/proxy-headers.conf;  # or inline the headers
        # WebSocket support (Grafana live):
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Enable and validate:

sudo ln -s /etc/nginx/sites-available/grafana.fogserv.cloud /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Certbot

# Initial issuance (uses the python3-certbot-nginx plugin to edit blocks,
# or use --webroot against /var/www/html as shown above)
sudo certbot --nginx -d grafana.fogserv.cloud

# Verify renewal timer
systemctl list-timers | grep certbot
sudo certbot renew --dry-run

If Cloudflare proxies (orange cloud) the record, use DNS validation instead of HTTP: certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/cf.ini -d grafana.fogserv.cloud. Details in letsencrypt-automation.

Upstreams instead of raw ports

For services with more than one replica or a non-obvious address:

upstream jellyfin {
    server 192.168.20.30:8096 max_fails=3 fail_timeout=10s;
    server 192.168.20.31:8096 backup;
    keepalive 16;
}

server {
    ...
    location / {
        proxy_pass http://jellyfin;
        proxy_set_header Connection "";   # required for upstream keepalive
    }
}

Practical Examples

Proxy three services with distinct backends

for svc in "grafana 3000" "uptime-kuma 3001" "forgejo 3002"; do
  set -- $svc
  echo "$1 -> 127.0.0.1:$2"
done

Each gets its own file under sites-available/ following the pattern above; the backend listens only on loopback so it is unreachable except through nginx โ€” enforce that in nftables (firewalls-nftables).

Verify a proxied service end to end

curl -sI https://grafana.fogserv.cloud | head -5
# HTTP/2 200 ... expect server: cloudflare at the edge, nginx behind it

Static site + API split

location /api/ { proxy_pass http://127.0.0.1:8080/; }   # trailing slash strips /api
location /     { root /srv/www/fogserv; try_files $uri $uri/ =404; }

Troubleshooting & Common Pitfalls

Problem Cause Fix
502 Bad Gateway Backend down or wrong port curl 127.0.0.1:<port> locally; check docker ps / systemd unit
504 Gateway Timeout Slow upstream past read timeout Raise proxy_read_timeout; check backend health
WebSocket drops Missing Upgrade headers Add Upgrade/Connection headers as above
Certbot fails behind Cloudflare proxy HTTP-01 blocked by CDN caching Use DNS-01 with cloudflare plugin, or grey-cloud during issuance
App sees 127.0.0.1 as client IP Missing forwarded headers Ensure X-Real-IP / X-Forwarded-For set; app must honor them
Config change breaks everything Typo in included snippet Always nginx -t before reload; never restart blindly

Next Steps / Ops Actions

Sources & Related

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse