G
GuideDevOps
Lesson 20 of 28

Reverse Proxy (Nginx, HAProxy)

Part of the Networking Basics tutorial series.

A reverse proxy sits in front of your backend servers, accepting client requests and forwarding them on. It's different from a forward proxy which sits in front of clients. Reverse proxies are essential for scalable, secure infrastructure.

Forward Proxy vs Reverse Proxy

Forward Proxy (client side):

Client → Forward Proxy → Internet → External Server
(Client wants to go OUT)

Example: Corporate proxy, VPN

Reverse Proxy (server side):

Internet/Client → Reverse Proxy → Backend Servers
(Server wants to RECEIVE protected)

Example: nginx, HAProxy in front of web servers

What is a Reverse Proxy?

Reverse Proxy = Server that forwards requests to backend servers:

Client makes request to: api.example.com
    ↓
Reverse Proxy receives it
    ├─ Checks cache (maybe has cached response)
    ├─ Routes to backend server (round-robin, least-conn, etc)
    ├─ Waits for response
    └─ Returns to client

Client never directly connects to backend

Why Use a Reverse Proxy?

1. Security

  • Backend servers hidden from internet
  • Firewall only needs to allow reverse proxy
  • Black-box defense against attacks

2. Load Balancing

  • Distribute requests across multiple backends
  • Add/remove servers without client awareness
  • Health checks automatically remove failures

3. Caching

  • Cache responses from backend
  • Reduce backend load
  • Faster responses for frequently accessed content

4. SSL/TLS Termination

  • Encryption work done at proxy
  • Backend servers don't need certificates
  • Simpler backend code

5. Rewrite/Transform

  • Modify requests/responses
  • Add security headers
  • Redirect based on URLs

6. Compression

  • Compress responses to clients
  • Save bandwidth
  • Invisible to clients

Reverse Proxy vs Load Balancer

AspectReverse ProxyLoad Balancer
PurposeRequest routing, caching, securityDistribute traffic equally
PlacementSingle entry pointBetween clients and servers
RoutingContent-based, URL-basedRound-robin, least-conn
CachingOften enabledNot typical
SSLOften terminates hereCan terminate here too
Use CaseAPI gateway, web serverHigh-traffic services

Often combined: Reverse proxy that also does load balancing!

nginx as Reverse Proxy

Basic Configuration:

server {
    listen 80;
    server_name api.example.com;
    
    location / {
        # Forward all requests to backend
        proxy_pass http://backend-server:8080;
        
        # Forward client IP
        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_set_header Host $host;
    }
}

With Backend Pool:

upstream backend {
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;
    server 192.168.1.103:8080;
}
 
server {
    listen 80;
    
    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
    }
}

With Caching:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m;
 
server {
    listen 80;
    
    location / {
        # Enable caching
        proxy_cache my_cache;
        proxy_cache_valid 200 1h;           # Cache 200 responses for 1 hour
        proxy_cache_valid 404 10m;          # Cache 404s for 10 minutes
        proxy_cache_use_stale error timeout; # Use stale if backend down
        
        # Add header showing cache status
        add_header X-Cache-Status $upstream_cache_status;
        
        proxy_pass http://backend;
    }
}

Content-Based Routing

Route based on URL path:

server {
    listen 80;
    server_name example.com;
    
    location /api/ {
        proxy_pass http://api-backend:8080;
    }
    
    location /static/ {
        proxy_pass http://static-backend:8080;
    }
    
    location /uploads/ {
        proxy_pass http://file-backend:9000;
    }
}

Route based on hostname:

server {
    server_name api.example.com;
    proxy_pass http://api-backend:8080;
}
 
server {
    server_name www.example.com;
    proxy_pass http://web-backend:8080;
}
 
server {
    server_name static.example.com;
    proxy_pass http://static-backend:8080;
}

Request modification

Rewrite URLs:

server {
    listen 80;
    
    # Rewrite /v1/users to /users on backend
    location /v1/ {
        rewrite ^/v1/(.*) /$1 break;
        proxy_pass http://backend:8080;
    }
    
    # Redirect /old-page to /new-page
    location /old-page {
        return 301 /new-page;
    }
}

Add Security Headers:

server {
    # Add security headers to all responses
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "strict-origin-when-cross-origin";
    add_header Permissions-Policy "geolocation=(), microphone=()";
    
    location / {
        proxy_pass http://backend:8080;
    }
}

Rate Limiting

Limit requests per IP:

# Define rate limit zone
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
 
server {
    listen 80;
    
    location /api/ {
        # Max 10 requests/second per IP
        limit_req zone=api_limit burst=20 nodelay;
        
        proxy_pass http://backend:8080;
    }
}

HAProxy as Reverse Proxy

global
    maxconn 4096
    
defaults
    mode http
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

frontend web_frontend
    bind *:80
    
    # Route based on path
    acl is_api path_beg /api/
    acl is_static path_beg /static/
    
    use_backend api_backend if is_api
    use_backend static_backend if is_static
    default_backend web_backend

backend web_backend
    balance roundrobin
    server web1 192.168.1.101:8080 check
    server web2 192.168.1.102:8080 check

backend api_backend
    balance leastconn
    server api1 192.168.1.201:8080 check
    server api2 192.168.1.202:8080 check

backend static_backend
    balance roundrobin
    server static1 192.168.1.251:9000 check

Caching Strategies

Cache Busting Strategy:

Version in URL: /static/app.v123.js
Browser caches forever
Update code: /static/app.v124.js
→ New URL, new request to server

Cache Conditional:

# Cache for 1 hour
proxy_cache_valid 200 1h;
 
# If backend returns 304 Not Modified
proxy_cache_valid "http_code 304" 1h;
 
# Check If-Modified-Since header
if-modified-since: conditional request

Troubleshooting

"Backend can't see real client IP"

Add to reverse proxy config:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
App reads X-Real-IP or X-Forwarded-For header

"Responses cached incorrectly"

1. Check Cache-Control headers from backend
   curl -i http://backend:8080/api/data
   → Look for: Cache-Control: no-cache, no-store

2. Disable caching for dynamic content
   proxy_no_cache $skip_cache;
   proxy_cache_bypass $skip_cache;

"Backend connection timeout"

Increase timeouts:
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;

Key Concepts

  • Reverse Proxy = Sits in front of backend servers
  • Forward Proxy = Sits in front of clients
  • Load Balancing = Distribute across backends
  • Caching = Store responses to reduce backend load
  • SSL Termination = Encryption at proxy, not backend
  • Content-based Routing = Route by URL, hostname
  • Rate Limiting = Protect against abuse
  • Security Headers = Protect against web attacks
  • Reverse proxy is transparent to clients
  • Always forward real client IP to backend