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:
- โ Write clean server blocks that proxy to local services
- โ Issue and renew certificates with certbot
- โ Apply gzip, buffering, and timeout tuning
- โ Debug 502/504 errors from proxying
Table of Contents
- Context / Why This Matters
- Implementation / Core Content
- Practical Examples
- Troubleshooting & Common Pitfalls
- 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
- Compare trade-offs: caddy-reverse-proxy and traefik-v3-reverse-proxy pick one proxy per host and stick to it.
- Review cipher suites and HSTS against tls-configuration.
- Rate-limit admin paths and pair with fail2ban: fail2ban-setup.
Sources & Related
External references consulted:
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation by KB build session.