A reverse proxy sits in front of your application servers, handling incoming requests and distributing them to backend services. It is essential for TLS termination, load balancing, caching, and security. This guide covers two of the most popular options: Nginx and Caddy.

Reverse Proxy Guide

Why Use a Reverse Proxy

  • TLS termination : Handle HTTPS once at the proxy layer.

  • Load balancing : Distribute traffic across multiple backend instances.

  • Caching : Cache responses to reduce backend load.

  • Security : Filter malicious requests, rate limiting, IP blocking.

  • Multiple services : Route different paths to different backends from one domain.

Nginx Reverse Proxy

Nginx is the industry standard for reverse proxying. It is mature, highly performant, and extremely configurable.

Basic Reverse Proxy Configuration

server {

listen 80;

server_name app.example.com;

location / {

proxy_pass http://127.0.0.1:3000;

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 proxy_pass directive sends requests to the backend. Always forward the original host and client IP headers so your application has accurate client information.

WebSocket Support

location /ws/ {

proxy_pass http://127.0.0.1:3001;

proxy_http_version 1.1;

proxy_set_header Upgrade $http_upgrade;

proxy_set_header Connection "upgrade";

proxy_set_header Host $host;

proxy_read_timeout 86400s;

}

The Upgrade and Connection headers are required for WebSocket connections. Set proxy_read_timeout to a long duration since WebSocket connections remain open.

Load Balancing

Distribute traffic across multiple backends:

upstream app_cluster {

least_conn;

server 10.0.0.1:3000 weight=3;

server 10.0.0.2:3000;

server 10.0.0.3:3000 backup;

}

server {

location / {

proxy_pass http://app_cluster;

}

}

Load balancing methods: round-robin (default), least_conn (fewest active connections), ip_hash (session persistence). Assign higher weight to more powerful servers.

Caching

Cache responses from the backend:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=appcache:10m max_size=1g;

server {

location / {

proxy_cache appcache;

proxy_cache_valid 200 30m;

proxy_cache_valid 404 1m;

proxy_cache_use_stale error timeout updating;

add_header X-Cache-Status $upstream_cache_status;

}

}

The $upstream_cache_status header helps debug caching (HIT, MISS, STALE, etc.). proxy_cache_use_stale serves stale content when the backend is down.

Caddy Reverse Proxy

Caddy is a modern web server with automatic HTTPS, simpler configuration, and Go-based performance. It is ideal for teams that want a zero-fuss reverse proxy.

Basic Reverse Proxy

Caddyfile

app.example.com {

reverse_proxy localhost:3000

}

That is the entire configuration. Caddy automatically obtains and renews Let's Encrypt TLS certificates.

Multiple Backends with Load Balancing

app.example.com {

reverse_proxy 10.0.0.1:3000 10.0.0.2:3000 10.0.0.3:3000 {

lb_policy least_conn

health_uri /health

health_interval 30s

}

}

Caddy supports multiple load balancing policies: random, least_conn, round_robin, first, ip_hash.

Path-Based Routing

Route different paths to different services:

api.example.com {

reverse_proxy /api/* localhost:3000

reverse_proxy /auth/* localhost:3001

reverse_proxy localhost:3002 # default

}

Request Manipulation

app.example.com {

reverse_proxy localhost:3000 {

header_up Host {host}

header_up X-Real-IP {remote_host}

}

Add security headers

header {

X-Frame-Options "SAMEORIGIN"

X-Content-Type-Options "nosniff"

}

}

Caddy's header directive works for both request and response headers.

Nginx vs Caddy: Comparison

| Feature | Nginx | Caddy |

|---------|-------|-------|

| Configuration | Complex, powerful | Simple, opinionated |

| TLS | Manual cert management | Automatic Let's Encrypt |

| Performance | Excellent | Very good |

| Ecosystem | Vast (modules, guides) | Growing but smaller |

| Docker support | Official image | Official image |

| Learning curve | Steep | Gentle |

| Dynamic configuration | Limited | REST API available |

| HTTP/3 | Supported | Supported |

Security Headers

Regardless of which reverse proxy you choose, add these security headers:

For Nginx:

add_header X-Frame-Options "SAMEORIGIN" always;

add_header X-Content-Type-Options "nosniff" always;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

add_header Content-Security-Policy "default-src 'self';" always;

For Caddy:

header {

X-Frame-Options "SAMEORIGIN"

X-Content-Type-Options "nosniff"

Strict-Transport-Security "max-age=31536000; includeSubDomains"

}

Rate Limiting

Nginx:

limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

location /api/ {

limit_req zone=api burst=20 nodelay;

proxy_pass http://backend;

}

Caddy:

app.example.com {

rate_limit {

zone api {

key {remote_host}

events 10

window 1s

}

}

reverse_proxy localhost:3000

}

Health Checks

Nginx health checks require the Plus version or are handled externally (e.g., via Docker health checks). Caddy includes built-in active health checks that mark unhealthy backends as down and stop routing traffic to them.

Summary

Nginx and Caddy are both excellent reverse proxies. Nginx offers unmatched flexibility and performance for complex deployments. Caddy provides automatic TLS and simpler configuration, making it ideal for smaller teams or simpler setups. For TLS-heavy deployments, Caddy's automatic certificates save significant operational overhead. For high-traffic scenarios requiring fine-grained control, Nginx remains the standard. Both can route traffic, terminate TLS, add security headers, and cache responses -- choose based on your team's expertise and operational complexity tolerance.