Reverse Proxy Basics - One Front Door for Every Service

Status: Active
Last Updated: 2026-08-26
Category: Networking - Reverse Proxies
Prerequisites: dns-explained, ../security/tls-configuration
Time: 2 hours
Tags: reverse-proxy, tls-termination, routing, headers, caddy, traefik, nginx

Summary

Why a self-hosted fleet funnels all public traffic through one reverse proxy: TLS termination in one place, name- and path-based routing to many backends on private ports, and correct forwarded headers. Concepts here apply to Caddy (caddy-reverse-proxy), Traefik (traefik-v3-reverse-proxy), and nginx (nginx-configuration) alike.

๐ŸŽฏ What You'll Learn

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


Table of Contents

  1. Context / Why This Matters
  2. What a Reverse Proxy Buys You
  3. Routing Models
  4. TLS Termination
  5. Forwarded Headers
  6. Troubleshooting & Common Pitfalls
  7. Next Steps / Ops Actions
  8. Sources & Related
  9. Change Log

Context / Why This Matters

The fogserv edge model is: Cloudflare โ†’ one reverse-proxy host โ†’ N internal services. Without it, every service would need its own public port, its own cert renewal, its own exposure in the firewall โ€” and Cloudflare's limited proxied ports make most of those unreachable anyway. The proxy concentrates ingress so the firewall has exactly one job on the public side (firewalls-nftables).

Internet โ”€โ”€443โ”€โ”€โ–ถ Cloudflare โ”€โ”€443โ”€โ”€โ–ถ Caddy/Traefik/nginx (edge)
                                        โ”œโ”€ grafana.fogserv.cloud โ†’ 127.0.0.1:3000
                                        โ”œโ”€ git.fogserv.cloud     โ†’ 10.10.0.11:3000
                                        โ””โ”€ jellyfin.fogserv.cloudโ†’ 10.10.0.21:8096

What a Reverse Proxy Buys You

Capability Effect
Single TLS endpoint One ACME/DNS-01 setup, wildcard cert, uniform cipher policy (../security/certificate-fundamentals)
Name-based routing Many services share 443; browsers see clean hostnames
Backend isolation Services bind to localhost/LAN only; never publicly exposed directly
Central access control Auth middleware, IP allowlists, rate limits applied per-route
Hiding topology Visitors never learn backend IPs or ports

A forward proxy sits in front of clients; a reverse proxy sits in front of servers. A plain router port forward does none of the above โ€” it just punches a hole.

Routing Models

Host-based (default choice) โ€” service.fogserv.cloud โ†’ backend:

Path-based โ€” fogserv.cloud/grafana โ†’ backend:

Decision rule: host-based unless there's a specific reason not to be.

TLS Termination

Terminate at the edge; talk plain HTTP to backends on the trusted LAN:

Browser โ‡TLSโ‡’ Edge proxy โ‡httpโ‡’ grafana:3000   (internal network trusted segment)

This is acceptable when backends live on an isolated services VLAN/overlay (virtual-networks). If traffic crosses untrusted links (between sites), either route through the WireGuard tunnel (which encrypts anyway) or use re-encryption (https upstreams with internal CA certs). Never terminate at Cloudflare then forward plain HTTP across the internet โ€” that's the Flexible-mode trap from cloudflare-dns.

Forwarded Headers

Backends must learn the real client info or logs break, redirects go to wrong schemes, and CSRF checks fail. The edge sets:

X-Forwarded-For: 203.0.113.5          # real client
X-Forwarded-Proto: https              # scheme the client used
X-Forwarded-Host: grafana.fogserv.cloud
Host: grafana.fogserv.cloud           # usually preserved as-is

Backend side: configure apps to trust these headers only from the proxy IP (otherwise clients can spoof them). Grafana example:

[server]
root_url = https://grafana.fogserv.cloud
[auth]
; trust proxy only from edge

Practical Examples

Trace what the proxy is doing:

# From outside: full request path + headers returned
curl -sv https://grafana.fogserv.cloud/api/health -o /dev/null 2>&1 | grep -E "^< |^> Host"

# On the edge: confirm the backend answers before blaming the proxy
curl -s http://127.0.0.1:3000/api/health

# Confirm real client IPs reach backend logs (not 127.0.0.1)
ssh edge "tail -f /var/log/*proxy*" & curl -s https://grafana.fogserv.cloud >/dev/null

Add a new service checklist:

  1. Backend binds to 127.0.0.1 or LAN IP only.
  2. DNS record name CNAME edge (proxied) โ€” cloudflare-dns.
  3. Route block in Caddy/Traefik/nginx config.
  4. Firewall: nothing extra publicly; only the edge listens.
  5. Test: curl -sv https://name.fogserv.cloud/api/health.

Troubleshooting & Common Pitfalls

Problem Cause Fix
App redirects to http:// or wrong port Missing/untrusted X-Forwarded-Proto Set headers at edge; enable app's "behind reverse proxy" mode
Logs show every client as proxy IP Headers set but app doesn't read them Configure trusted proxies in app
502 Bad Gateway Backend down/wrong port/binding to 127.0.0.1 while proxy targets LAN IP curl backend directly from edge host first
Works via HTTP but breaks over HTTPS Mixed content: app emits absolute http:// asset URLs Set root_url/base URL; check CSP errors in browser console
Websockets disconnect Proxy lacks upgrade handling Enable websocket support (Caddy/Traefik auto; nginx needs Upgrade headers map)
Two proxies fight over port 80/443 Legacy nginx + new Caddy on same host One edge proxy per host; move others behind it

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