edit_document// BLOG_POST.md

Nginx Reverse Proxy: The Configuration Every Developer Should Know

//

, ,

Most web applications do not serve traffic directly. Nginx (or a similar reverse proxy) sits between the internet and your application, handling responsibilities your app should not: TLS termination, load balancing across multiple instances, static file serving, request rate limiting, response compression, and connection management. Understanding Nginx configuration is not optional for backend developers deploying real services. Here is the configuration from scratch.

What a Reverse Proxy Does

A reverse proxy accepts incoming client requests and forwards them to backend servers. The client never communicates directly with your Node.js, Python, or Go process. This architecture provides several benefits: TLS can be terminated at the proxy (your app serves plain HTTP internally), multiple app instances can be load-balanced behind a single domain, static assets can be served from disk without hitting your application, and the proxy can buffer slow clients so your app threads are freed quickly.

Basic Reverse Proxy Configuration

# /etc/nginx/sites-available/myapp.conf

# Upstream block defines your backend server(s)
upstream app_backend {
    # Multiple servers for load balancing
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;

    # Keepalive connections to backends (reduces TCP handshakes)
    keepalive 32;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

# Main HTTPS server block
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    # TLS certificates (Let's Encrypt / Certbot)
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Modern TLS configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml;
    gzip_min_length 1000;
    gzip_vary on;

    # Proxy all requests to the backend
    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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;

        # Timeouts
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

Serving Static Files Directly

Nginx serves static files from disk orders of magnitude faster than your application process. Intercept static asset requests before they reach the proxy:

    # Serve static files directly from disk
    location /static/ {
        alias /var/www/myapp/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Serve uploaded media
    location /uploads/ {
        alias /var/www/myapp/uploads/;
        expires 30d;
        add_header Cache-Control "public";
    }

    # Cache-bust hashed assets aggressively
    location ~* .(js|css|png|jpg|jpeg|gif|svg|ico|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

Rate Limiting

# Define rate limit zone in http block (nginx.conf)
http {
    # 10 requests per second per IP, burst up to 20
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login_limit:10m rate=1r/s;
}

# Apply in server block
server {
    # General API rate limit
    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;
        limit_req_status 429;
        proxy_pass http://app_backend;
    }

    # Strict rate limit on auth endpoints
    location /api/auth/ {
        limit_req zone=login_limit burst=5 nodelay;
        limit_req_status 429;
        proxy_pass http://app_backend;
    }
}

Load Balancing Strategies

# Round-robin (default): distributes evenly
upstream backend_rr {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

# Least connections: sends to the server with fewest active connections
upstream backend_lc {
    least_conn;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

# IP hash: same client always hits same server (session affinity)
upstream backend_ip {
    ip_hash;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

# Weighted: send more traffic to beefier servers
upstream backend_weighted {
    server 127.0.0.1:3000 weight=3;  # Gets 3x traffic
    server 127.0.0.1:3001 weight=1;
}

WebSocket Support

WebSocket connections require the Upgrade and Connection headers to be forwarded. The proxy config above already includes this, but here is the dedicated pattern for a WebSocket-specific path:

    location /ws/ {
        proxy_pass http://app_backend;
        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;  # Keep WS connections alive for 24h
        proxy_send_timeout 86400s;
    }

Testing and Reloading

# Test configuration for syntax errors before reloading
sudo nginx -t

# Reload without dropping connections (graceful)
sudo nginx -s reload

# View error logs
sudo tail -f /var/log/nginx/error.log

Always run nginx -t before reloading. A syntax error in any included config file will prevent Nginx from starting, taking your entire site offline. In CI/CD pipelines, run the test as a validation step before deploying config changes.

Further reading: Nginx Proxy Module | Nginx Load Balancing | Mozilla SSL Configuration Generator


arrow_circle_right// POST_NAVIGATION

forum// COMMENTS

Leave a Reply

Your email address will not be published. Required fields are marked *