Claude Skill

nginx-ops

Nginx configuration, reverse proxy, SSL/TLS, load balancing, and performance tuning. Use for: nginx, reverse proxy, load balancer, proxy_pass, ssl certificate, lets encrypt, web server, location block, upstream, server block, nginx config, certbot, hsts, gzip, rate limiting.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_nginx-ops-3dfaf0b.zip · 29 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/nginx-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Nginx Operations

Comprehensive Nginx configuration, reverse proxy patterns, SSL/TLS hardening, load balancing strategies, and performance optimization for production deployments.


Configuration Architecture Quick Reference

nginx.conf (main context)
├── worker_processes auto;
├── worker_rlimit_nofile 65535;
│
├── events {                          # Connection handling
│   ├── worker_connections 4096;
│   └── multi_accept on;
│   }
│
├── http {                            # HTTP server settings
│   ├── include mime.types;
│   ├── default_type application/octet-stream;
│   ├── sendfile on;
│   ├── gzip on;
│   │
│   ├── upstream backend {            # Load balancing pool
│   │   └── server 127.0.0.1:3000;
│   │   }
│   │
│   ├── server {                      # Virtual host
│   │   ├── listen 443 ssl;
│   │   ├── server_name example.com;
│   │   │
│   │   ├── location / {              # Request routing
│   │   │   └── proxy_pass http://backend;
│   │   │   }
│   │   │
│   │   └── location /static/ {
│   │       └── root /var/www;
│   │       }
│   │   }
│   │
│   └── include /etc/nginx/conf.d/*.conf;
│   }
│
└── stream {                          # TCP/UDP proxying (optional)
    └── server { ... }
    }

Directive Inheritance Rules

Rule Behavior Example
Inherit down Child blocks inherit parent directives gzip on; in http applies to all server blocks
Override Child directive overrides parent gzip off; in location overrides http-level gzip on;
Array directives NOT inherited - must be redeclared proxy_set_header in location replaces ALL headers from server
No upward Inner blocks never affect outer location-level settings don't affect server

Critical: Array-type directives (proxy_set_header, add_header, proxy_hide_header) are completely replaced when redefined in a child block, not merged. If you set one proxy_set_header in a location, you must redeclare ALL of them.


Reverse Proxy Decision Tree

Need to proxy requests?
│
├─ Single backend server?
│  └─ Use simple proxy_pass
│     proxy_pass http://127.0.0.1:3000;
│
├─ Multiple backend servers?
│  │
│  ├─ Need session persistence?
│  │  ├─ By client IP → ip_hash
│  │  └─ By cookie    → sticky cookie (Nginx Plus)
│  │
│  ├─ Backends have unequal capacity?
│  │  └─ Use weight parameter
│  │     server backend1:3000 weight=3;
│  │     server backend2:3000 weight=1;
│  │
│  ├─ Want fewest active connections?
│  │  └─ least_conn
│  │
│  ├─ Want even random distribution?
│  │  └─ random two least_conn
│  │
│  └─ Default (no special needs)?
│     └─ round-robin (default, no directive needed)
│
├─ WebSocket connections?
│  └─ Add Upgrade + Connection headers
│     proxy_set_header Upgrade $http_upgrade;
│     proxy_set_header Connection "upgrade";
│
├─ gRPC backend?
│  └─ Use grpc_pass grpc://backend;
│
└─ Streaming / Server-Sent Events?
   └─ Disable buffering
      proxy_buffering off;

SSL/TLS Quick Start

Let's Encrypt with Certbot

# Install certbot
sudo apt install certbot python3-certbot-nginx   # Debian/Ubuntu
sudo dnf install certbot python3-certbot-nginx    # RHEL/Fedora

# Obtain certificate (nginx plugin - easiest)
sudo certbot --nginx -d example.com -d www.example.com

# Obtain certificate (webroot - no nginx restart)
sudo certbot certonly --webroot -w /var/www/html -d example.com

# Test auto-renewal
sudo certbot renew --dry-run

Minimal Production SSL Config

server {
    listen 443 ssl http2;
    server_name example.com;

    # Certificates
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Modern TLS (1.2 + 1.3)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # HSTS (2 years)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    # Session caching
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    root /var/www/example.com;
    index index.html;
}

# HTTP → HTTPS redirect
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

Location Matching Order

Nginx evaluates location blocks in a specific priority order, not in the order they appear in the config file.

Priority Modifier Type Example Behavior
1 = Exact match location = /favicon.ico Stops search immediately on match
2 ^~ Prefix (no regex) location ^~ /static/ Stops search if this prefix matches (skips regex)
3 ~ Regex (case-sensitive) location ~ \.php$ First matching regex wins
3 ~* Regex (case-insensitive) location ~* \.(jpg\|png)$ First matching regex wins
4 (none) Prefix location /api/ Longest prefix wins (but only after regex check)

Evaluation Algorithm

  1. Check all prefix locations, remember the longest match
  2. If longest match has ^~ modifier → use it, stop
  3. Check regex locations in config-file order → first match wins
  4. If no regex matches → use the longest prefix from step 1
  5. = /path is checked first and wins immediately if matched

Example

location = /             { }  # Only exact "/"
location /               { }  # Catch-all prefix
location /api/           { }  # Prefix: /api/*
location ^~ /static/     { }  # Prefix, skip regex: /static/*
location ~ \.php$        { }  # Regex: any .php file
location ~* \.(gif|jpg)$ { }  # Case-insensitive regex: images
Request URI Matched Location Why
/ = / Exact match (priority 1)
/index.html / Longest prefix, no regex match
/api/users /api/ Longest prefix, no regex match
/static/logo.png ^~ /static/ ^~ skips regex check
/app/index.php ~ \.php$ Regex beats prefix
/photos/cat.jpg ~* \.(gif\|jpg)$ Regex beats prefix

Common Configurations

SPA Routing (React, Vue, Angular)

server {
    listen 80;
    server_name app.example.com;

    root /var/www/app/dist;
    index index.html;

    # Serve static files directly, fall back to index.html for SPA routes
    location / {
        try_files $uri $uri/ /index.html;
    }

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

WebSocket Proxy

location /ws/ {
    proxy_pass http://127.0.0.1:3000;
    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_read_timeout 86400s;  # Keep WebSocket alive for 24h
    proxy_send_timeout 86400s;
}

Rate Limiting

# Define zone: 10MB shared memory, 10 requests/second per IP
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

server {
    location /api/ {
        # Allow burst of 20, process excess without delay up to burst
        limit_req zone=api burst=20 nodelay;
        limit_req_status 429;

        proxy_pass http://backend;
    }
}

Gzip Compression

http {
    gzip on;
    gzip_comp_level 5;          # Balance CPU vs compression (1-9)
    gzip_min_length 256;        # Don't compress tiny responses
    gzip_vary on;               # Vary: Accept-Encoding header
    gzip_proxied any;           # Compress proxied responses too
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        application/xml+rss
        image/svg+xml;
}

Static File Serving

location /static/ {
    alias /var/www/static/;     # Note: alias, not root (includes /static/ path)
    expires 30d;
    add_header Cache-Control "public, no-transform";

    # Disable access log for static files
    access_log off;

    # Enable open file cache
    open_file_cache max=1000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
}

CORS Headers

location /api/ {
    # CORS headers
    add_header Access-Control-Allow-Origin "https://app.example.com" always;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
    add_header Access-Control-Max-Age 86400 always;

    # Handle preflight requests
    if ($request_method = OPTIONS) {
        return 204;
    }

    proxy_pass http://backend;
}

Docker Patterns

Nginx as Reverse Proxy in Docker Compose

# docker-compose.yml
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - app
    networks:
      - webnet

  app:
    build: .
    expose:
      - "3000"    # Internal only, not published to host
    networks:
      - webnet

networks:
  webnet:
# nginx.conf for docker-compose (use service name as hostname)
upstream app_backend {
    server app:3000;    # Docker DNS resolves service name
}

server {
    listen 80;
    location / {
        proxy_pass http://app_backend;
        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;
    }
}

Multi-Stage Build with Static Assets

# Stage 1: Build frontend
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Serve with nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
# nginx.conf for containerized SPA
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    # SPA routing
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Health check endpoint
    location /health {
        access_log off;
        return 200 "OK\n";
        add_header Content-Type text/plain;
    }

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

Common Gotchas

Gotcha Why Fix
Trailing slash in proxy_pass proxy_pass http://backend keeps /api/users as-is; proxy_pass http://backend/ strips the matched location prefix Be intentional: with / to strip prefix, without to preserve
Missing proxy headers Backend sees nginx's IP, not the client's. Breaks auth, logging, and geo detection Always set X-Real-IP, X-Forwarded-For, X-Forwarded-Proto, and Host
Buffer size errors (502) Large headers (cookies, JWTs) exceed default buffer sizes Increase proxy_buffer_size 8k; and proxy_buffers 4 16k;
worker_connections too low Default is 512 or 1024; each client uses 2 connections (client + upstream) Set worker_connections 4096; and raise worker_rlimit_nofile
try_files with proxy_pass try_files and proxy_pass in the same location don't work as expected Use try_files $uri @backend; with a named location for proxy
"if is evil" if inside location creates an implicit nested location, breaking directives Use map for variable-based logic; reserve if for return/rewrite only
Resolver for dynamic upstreams Variables in proxy_pass (e.g., $upstream) bypass startup DNS resolution Add resolver 127.0.0.11 valid=30s; (Docker) or resolver 1.1.1.1;
Missing index directive Returns 403 Forbidden when accessing a directory instead of index file Add index index.html; in server or location block
Permission denied on socket Nginx worker can't read the upstream Unix socket Ensure nginx user is in the socket's group; chmod 660 the socket
Duplicate Content-Encoding with gzip Upstream already compresses + nginx gzip double-compresses Use gzip_proxied carefully or proxy_set_header Accept-Encoding "";
add_header not inherited Adding ANY add_header in a location discards ALL parent add_header directives Redeclare all headers in the child block, or use include for shared headers
alias vs root confusion root appends the location path; alias replaces it. /img/ + root /data = /data/img/; alias /data/ = /data/ Use alias when location path shouldn't appear in filesystem path

Reference Files

File Contents Lines
reverse-proxy.md Upstream blocks, load balancing, proxy caching, WebSocket/gRPC, timeouts, real-world configs ~650
ssl-security.md TLS config, Let's Encrypt, HSTS, OCSP, security headers, rate limiting, mTLS ~550
performance.md Worker tuning, compression, caching, HTTP/2+3, static files, monitoring ~550

See Also

Files (claude-mods)
  • assets
    • .gitkeep 0 B · in bundle
  • references
    • performance.md 25.1 KB
      # Performance Reference
      
      Comprehensive guide to Nginx performance optimization: worker tuning, connection handling, compression, caching, HTTP/2 and HTTP/3, static file serving, and monitoring.
      
      ---
      
      ## Table of Contents
      
      1. [Worker Configuration](#worker-configuration)
      2. [Event Model](#event-model)
      3. [Connection Handling](#connection-handling)
      4. [Sendfile and TCP Optimizations](#sendfile-and-tcp-optimizations)
      5. [Compression](#compression)
      6. [Open File Cache](#open-file-cache)
      7. [Static File Serving](#static-file-serving)
      8. [Proxy Caching](#proxy-caching)
      9. [FastCGI Caching](#fastcgi-caching)
      10. [Microcaching](#microcaching)
      11. [HTTP/2](#http2)
      12. [HTTP/3 (QUIC)](#http3-quic)
      13. [Connection Draining](#connection-draining)
      14. [Monitoring](#monitoring)
      
      ---
      
      ## Worker Configuration
      
      ### Worker Processes
      
      ```nginx
      # Auto-detect CPU cores (recommended)
      worker_processes auto;
      
      # Or set explicitly (match CPU core count)
      # worker_processes 4;
      
      # Pin workers to specific CPUs (optional, advanced)
      worker_cpu_affinity auto;
      # Or manually: worker_cpu_affinity 0001 0010 0100 1000;
      ```
      
      ### Worker Connections
      
      ```nginx
      events {
          # Maximum simultaneous connections per worker
          # Total capacity = worker_processes * worker_connections
          worker_connections 4096;
      
          # Accept multiple connections at once
          multi_accept on;
      }
      ```
      
      ### File Descriptor Limits
      
      Each connection uses at least one file descriptor (two when proxying).
      
      ```nginx
      # Maximum open files per worker process
      # Should be >= 2 * worker_connections
      worker_rlimit_nofile 65535;
      ```
      
      Also set OS-level limits:
      
      ```bash
      # /etc/security/limits.conf
      nginx soft nofile 65535
      nginx hard nofile 65535
      
      # Or /etc/systemd/system/nginx.service.d/override.conf
      [Service]
      LimitNOFILE=65535
      ```
      
      ### Sizing Guidelines
      
      | Traffic Level | `worker_processes` | `worker_connections` | `worker_rlimit_nofile` | Total Capacity |
      |--------------|-------------------|---------------------|----------------------|----------------|
      | Low (< 1K rps) | auto (2-4) | 1024 | 4096 | 2K-4K connections |
      | Medium (1K-10K rps) | auto (4-8) | 4096 | 16384 | 16K-32K connections |
      | High (10K-100K rps) | auto (8-16) | 8192 | 65535 | 64K-128K connections |
      
      ---
      
      ## Event Model
      
      ### Linux (epoll)
      
      ```nginx
      events {
          use epoll;
          worker_connections 4096;
          multi_accept on;
      }
      ```
      
      `epoll` is the most efficient event model on Linux, using O(1) event notification.
      
      ### BSD/macOS (kqueue)
      
      ```nginx
      events {
          use kqueue;
          worker_connections 4096;
          multi_accept on;
      }
      ```
      
      ### Event Model Comparison
      
      | Model | OS | Scalability | Notes |
      |-------|-----|-------------|-------|
      | `epoll` | Linux 2.6+ | Excellent | Default and best for Linux |
      | `kqueue` | FreeBSD, macOS | Excellent | Default for BSD systems |
      | `select` | All | Poor | Legacy, limited to 1024 fds |
      | `poll` | All | Fair | Better than select, still O(n) |
      
      Nginx auto-selects the best available model. Explicit `use` is optional but recommended for clarity.
      
      ---
      
      ## Connection Handling
      
      ### Keepalive Configuration
      
      ```nginx
      http {
          # Client-facing keepalive
          keepalive_timeout 65s;        # Close idle connections after 65s
          keepalive_requests 1000;      # Max requests per keepalive connection
      
          # Reset timed-out connections (free resources faster)
          reset_timedout_connection on;
      
          # Client timeouts
          client_body_timeout 12s;      # Time to receive request body
          client_header_timeout 12s;    # Time to receive request headers
          send_timeout 10s;             # Time between successive writes to client
      
          # Limit request/header sizes
          client_max_body_size 10m;     # Max upload size
          client_body_buffer_size 16k;  # Buffer for request body
          client_header_buffer_size 1k; # Buffer for request headers
          large_client_header_buffers 4 8k;  # For large headers (cookies, etc.)
      }
      ```
      
      ### Keepalive Tuning
      
      | Scenario | `keepalive_timeout` | `keepalive_requests` | Rationale |
      |----------|--------------------|--------------------|-----------|
      | API server | 30-60s | 1000-10000 | Frequent requests, reuse connections |
      | Static files | 15-30s | 100-500 | Quick downloads, then disconnect |
      | WebSocket | 3600s+ | N/A | Long-lived connections |
      | High-traffic | 15-30s | 100 | Free connections sooner |
      
      ---
      
      ## Sendfile and TCP Optimizations
      
      ### sendfile
      
      Transfers files directly in kernel space without copying to userspace. Significant performance improvement for static files.
      
      ```nginx
      http {
          # Enable kernel-level file transfer
          sendfile on;
      
          # Send headers and beginning of file in one packet
          tcp_nopush on;
      
          # Disable Nagle algorithm (send small packets immediately)
          tcp_nodelay on;
      }
      ```
      
      ### How They Work Together
      
      | Directive | Purpose | When Active |
      |-----------|---------|-------------|
      | `sendfile on` | Zero-copy file transfer via kernel | Serving static files |
      | `tcp_nopush on` | Batch headers + file data into full packets | With sendfile, before last packet |
      | `tcp_nodelay on` | Send last packet immediately (no 200ms Nagle delay) | After tcp_nopush releases last packet |
      
      The combination `sendfile on; tcp_nopush on; tcp_nodelay on;` is optimal:
      1. `sendfile` transfers the file efficiently
      2. `tcp_nopush` fills packets completely for the bulk of the transfer
      3. `tcp_nodelay` sends the final partial packet without waiting
      
      ---
      
      ## Compression
      
      ### Gzip Configuration
      
      ```nginx
      http {
          # Enable gzip compression
          gzip on;
      
          # Compression level (1-9, higher = smaller but more CPU)
          # 5-6 is a good balance
          gzip_comp_level 5;
      
          # Minimum response size to compress (skip tiny responses)
          gzip_min_length 256;
      
          # Add Vary: Accept-Encoding header
          gzip_vary on;
      
          # Compress proxied responses
          gzip_proxied any;
      
          # MIME types to compress (text/html is always compressed)
          gzip_types
              text/plain
              text/css
              text/javascript
              text/xml
              application/javascript
              application/json
              application/xml
              application/xml+rss
              application/atom+xml
              application/vnd.ms-fontobject
              font/opentype
              image/svg+xml
              image/x-icon;
      
          # Disable gzip for old browsers (IE6)
          gzip_disable "msie6";
      
          # Buffer size for gzip
          gzip_buffers 16 8k;
      }
      ```
      
      ### Gzip Level Comparison
      
      | Level | Compression Ratio | CPU Usage | Best For |
      |-------|------------------|-----------|----------|
      | 1 | Low (~60%) | Minimal | Very high traffic, CPU-bound |
      | 3-4 | Medium (~70%) | Low | Good default for most sites |
      | 5-6 | Good (~75%) | Moderate | Recommended balance |
      | 9 | Maximum (~78%) | High | Rarely worth it over level 6 |
      
      The diminishing returns above level 5-6 are significant: going from level 5 to 9 might save 3% more bytes but costs 3-4x more CPU.
      
      ### Brotli Compression
      
      Brotli achieves 15-20% better compression than gzip at similar CPU cost. Requires the `ngx_brotli` module.
      
      ```nginx
      # Requires: ngx_brotli module
      # Install: https://github.com/google/ngx_brotli
      
      http {
          # Brotli dynamic compression
          brotli on;
          brotli_comp_level 6;
          brotli_min_length 256;
          brotli_types
              text/plain
              text/css
              text/javascript
              application/javascript
              application/json
              application/xml
              image/svg+xml;
      
          # Serve pre-compressed .br files if available
          brotli_static on;
      
          # Keep gzip as fallback (not all clients support brotli)
          gzip on;
          gzip_comp_level 5;
          gzip_types text/plain text/css application/javascript application/json;
      }
      ```
      
      ### Pre-Compressed Files
      
      Serve pre-compressed files to avoid runtime compression overhead.
      
      ```nginx
      http {
          # Serve .gz files if they exist
          gzip_static on;
      
          # Serve .br files if they exist (requires brotli module)
          brotli_static on;
      }
      ```
      
      Build step to pre-compress:
      
      ```bash
      # Pre-compress static assets during build
      fd -e js -e css -e html -e svg -e json dist/ -x gzip -k -9 {}
      fd -e js -e css -e html -e svg -e json dist/ -x brotli -k {}
      ```
      
      ---
      
      ## Open File Cache
      
      Cache file descriptors, metadata, and lookup results to reduce filesystem calls.
      
      ```nginx
      http {
          # Cache up to 1000 file descriptors, remove unused after 20s
          open_file_cache max=1000 inactive=20s;
      
          # How often to check if cached info is still valid
          open_file_cache_valid 30s;
      
          # Minimum number of accesses before caching
          open_file_cache_min_uses 2;
      
          # Cache file lookup errors (e.g., file not found)
          open_file_cache_errors on;
      }
      ```
      
      ### When to Use
      
      | Scenario | Recommended |
      |----------|-------------|
      | Serving many static files | Yes |
      | Reverse proxy only | No (not needed) |
      | High-traffic static site | Yes, increase max |
      | Few large files | Marginal benefit |
      
      ---
      
      ## Static File Serving
      
      ### Optimized Static File Configuration
      
      ```nginx
      server {
          listen 443 ssl http2;
          server_name static.example.com;
      
          root /var/www/static;
      
          # Performance fundamentals
          sendfile on;
          tcp_nopush on;
          tcp_nodelay on;
      
          # File descriptor caching
          open_file_cache max=2000 inactive=30s;
          open_file_cache_valid 60s;
          open_file_cache_min_uses 2;
          open_file_cache_errors on;
      
          # Immutable hashed assets (e.g., app.a3b4c5d6.js)
          location ~* \.[a-f0-9]{8,}\.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ {
              expires max;
              add_header Cache-Control "public, immutable";
              access_log off;
          }
      
          # Regular static assets
          location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
              expires 30d;
              add_header Cache-Control "public, no-transform";
              access_log off;
          }
      
          # HTML files (shorter cache, must revalidate)
          location ~* \.html$ {
              expires 1h;
              add_header Cache-Control "public, must-revalidate";
          }
      
          # Enable ETag for cache validation
          etag on;
      }
      ```
      
      ### Cache-Control Header Reference
      
      | Directive | Purpose | Use Case |
      |-----------|---------|----------|
      | `public` | Any cache can store | Static assets |
      | `private` | Only browser can store | User-specific content |
      | `no-cache` | Must revalidate before using | HTML pages |
      | `no-store` | Don't cache at all | Sensitive data |
      | `max-age=N` | Cache for N seconds | All cacheable content |
      | `immutable` | Never changes (skip revalidation) | Hashed filenames |
      | `must-revalidate` | Don't serve stale, even if disconnected | Critical content |
      | `stale-while-revalidate=N` | Serve stale while fetching fresh | UX optimization |
      
      ### Expires Directive Shortcuts
      
      ```nginx
      # Specific durations
      expires 30d;              # 30 days
      expires 1h;               # 1 hour
      expires 30m;              # 30 minutes
      expires max;              # Far future (practically forever)
      expires off;              # Don't add Expires header
      expires -1;               # Already expired (forces revalidation)
      expires epoch;            # Set to Unix epoch (Jan 1, 1970)
      expires modified +24h;   # 24h after file modification time
      ```
      
      ---
      
      ## Proxy Caching
      
      ### Production Proxy Cache Configuration
      
      ```nginx
      http {
          # Define cache storage
          proxy_cache_path /var/cache/nginx/proxy
              levels=1:2
              keys_zone=proxy_cache:20m      # 20MB metadata (~160K keys)
              max_size=20g                    # 20GB max disk usage
              inactive=7d                    # Remove unused items after 7 days
              use_temp_path=off              # Write directly to cache dir
              manager_files=100              # Files to process per cache manager cycle
              manager_threshold=200ms;       # Max time for cache manager cycle
      
          server {
              location / {
                  proxy_pass http://backend;
      
                  # Enable caching
                  proxy_cache proxy_cache;
      
                  # Cache key (determines what is considered a unique response)
                  proxy_cache_key "$scheme$request_method$host$request_uri";
      
                  # Cache durations by status code
                  proxy_cache_valid 200 301 302 1h;
                  proxy_cache_valid 404         1m;
      
                  # Serve stale content during backend errors
                  proxy_cache_use_stale error timeout updating
                                        http_500 http_502 http_503 http_504;
      
                  # Background refresh
                  proxy_cache_background_update on;
      
                  # Prevent thundering herd (only one request refreshes)
                  proxy_cache_lock on;
                  proxy_cache_lock_timeout 5s;
                  proxy_cache_lock_age 5s;
      
                  # Skip caching for logged-in users
                  proxy_cache_bypass $cookie_session $http_authorization;
                  proxy_no_cache $cookie_session $http_authorization;
      
                  # Show cache status in response header
                  add_header X-Cache-Status $upstream_cache_status always;
      
                  # Minimum uses before caching (prevent caching one-time requests)
                  proxy_cache_min_uses 2;
              }
          }
      }
      ```
      
      ### Cache Status Values
      
      The `$upstream_cache_status` variable contains:
      
      | Value | Meaning |
      |-------|---------|
      | `HIT` | Served from cache |
      | `MISS` | Not in cache, fetched from backend |
      | `BYPASS` | Cache was bypassed (proxy_cache_bypass matched) |
      | `EXPIRED` | Cache entry expired, fetched fresh from backend |
      | `STALE` | Served stale (backend unavailable, using proxy_cache_use_stale) |
      | `UPDATING` | Stale entry served while background update in progress |
      | `REVALIDATED` | Cache entry was revalidated with If-Modified-Since |
      
      ### Cache Key Design
      
      ```nginx
      # Default: includes method, scheme, host, and URI
      proxy_cache_key "$scheme$request_method$host$request_uri";
      
      # Include query parameters explicitly
      proxy_cache_key "$host$request_uri$is_args$args";
      
      # Include a custom header (e.g., API version)
      proxy_cache_key "$host$request_uri$http_x_api_version";
      
      # Include cookie for per-user caching (use carefully!)
      proxy_cache_key "$host$request_uri$cookie_lang";
      
      # Separate cache for mobile vs desktop
      proxy_cache_key "$host$request_uri$http_user_agent_class";
      ```
      
      ---
      
      ## FastCGI Caching
      
      For PHP-FPM and other FastCGI applications.
      
      ```nginx
      http {
          # FastCGI cache zone
          fastcgi_cache_path /var/cache/nginx/fastcgi
              levels=1:2
              keys_zone=fcgi_cache:10m
              max_size=5g
              inactive=60m
              use_temp_path=off;
      
          server {
              # Skip cache for logged-in users and POST requests
              set $skip_cache 0;
      
              # Don't cache POST requests
              if ($request_method = POST) {
                  set $skip_cache 1;
              }
      
              # Don't cache URLs with query strings
              if ($query_string != "") {
                  set $skip_cache 1;
              }
      
              # Don't cache admin pages (WordPress example)
              if ($request_uri ~* "/wp-admin/|/wp-login.php") {
                  set $skip_cache 1;
              }
      
              # Don't cache logged-in users (WordPress)
              if ($http_cookie ~* "wordpress_logged_in") {
                  set $skip_cache 1;
              }
      
              location ~ \.php$ {
                  fastcgi_pass php-fpm;
                  fastcgi_index index.php;
                  fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
                  include fastcgi_params;
      
                  # Enable FastCGI cache
                  fastcgi_cache fcgi_cache;
                  fastcgi_cache_key "$scheme$request_method$host$request_uri";
                  fastcgi_cache_valid 200 60m;
                  fastcgi_cache_valid 301 302 10m;
                  fastcgi_cache_valid 404 1m;
      
                  # Skip cache conditions
                  fastcgi_cache_bypass $skip_cache;
                  fastcgi_no_cache $skip_cache;
      
                  # Serve stale during errors
                  fastcgi_cache_use_stale error timeout updating
                                          http_500 http_502 http_503;
                  fastcgi_cache_background_update on;
                  fastcgi_cache_lock on;
      
                  # Cache status header
                  add_header X-FastCGI-Cache $upstream_cache_status;
              }
          }
      }
      ```
      
      ---
      
      ## Microcaching
      
      Cache dynamic content for very short durations (1-5 seconds) to absorb traffic spikes. Even a 1-second cache dramatically reduces backend load under high traffic.
      
      ```nginx
      http {
          proxy_cache_path /var/cache/nginx/micro
              levels=1:2
              keys_zone=micro_cache:5m
              max_size=1g
              inactive=1m
              use_temp_path=off;
      
          server {
              location / {
                  proxy_pass http://backend;
      
                  # Enable microcaching
                  proxy_cache micro_cache;
                  proxy_cache_valid 200 1s;    # Cache for just 1 second
      
                  # Serve stale while updating
                  proxy_cache_use_stale updating error timeout;
                  proxy_cache_background_update on;
      
                  # Only one request triggers backend fetch
                  proxy_cache_lock on;
                  proxy_cache_lock_timeout 1s;
      
                  # Don't cache if backend sets Cache-Control: no-cache
                  proxy_cache_bypass $http_cache_control;
      
                  # Don't cache for authenticated users
                  proxy_cache_bypass $cookie_session;
                  proxy_no_cache $cookie_session;
      
                  add_header X-Cache-Status $upstream_cache_status;
              }
          }
      }
      ```
      
      ### Microcaching Impact
      
      | Requests/sec | Without Cache | 1s Microcache | Reduction |
      |-------------|---------------|---------------|-----------|
      | 100 | 100 backend hits/s | 1 backend hit/s | 99% |
      | 1,000 | 1,000 backend hits/s | 1 backend hit/s | 99.9% |
      | 10,000 | 10,000 backend hits/s | 1 backend hit/s | 99.99% |
      
      ---
      
      ## HTTP/2
      
      ### Basic HTTP/2 Configuration
      
      ```nginx
      server {
          # http2 directive (Nginx 1.25.1+)
          listen 443 ssl;
          http2 on;
      
          # For older Nginx versions:
          # listen 443 ssl http2;
      
          server_name example.com;
      
          ssl_certificate     /etc/nginx/certs/fullchain.pem;
          ssl_certificate_key /etc/nginx/certs/privkey.pem;
      
          # HTTP/2 specific settings
          http2_max_concurrent_streams 128;
          http2_recv_buffer_size 256k;
      }
      ```
      
      ### HTTP/2 Benefits
      
      | Feature | HTTP/1.1 | HTTP/2 |
      |---------|---------|--------|
      | Multiplexing | 6 connections per domain | Unlimited streams on 1 connection |
      | Header compression | None | HPACK compression |
      | Server push | Not possible | Supported (but deprecated) |
      | Stream priority | N/A | Priority hints |
      | Binary protocol | Text-based | Binary framing |
      
      ### HTTP/2 Server Push (Deprecated)
      
      Server push was removed from Chrome and is generally considered deprecated. Use `<link rel="preload">` or `103 Early Hints` instead.
      
      ```nginx
      # 103 Early Hints (modern alternative to server push)
      location / {
          # Send early hints before the main response
          add_header Link "</style.css>; rel=preload; as=style" early;
          add_header Link "</app.js>; rel=preload; as=script" early;
      
          proxy_pass http://backend;
      }
      ```
      
      ---
      
      ## HTTP/3 (QUIC)
      
      HTTP/3 uses QUIC (UDP-based transport) for faster connection establishment and better performance on lossy networks.
      
      ### Basic HTTP/3 Configuration
      
      Requires Nginx 1.25.0+ compiled with QUIC support, or nginx-quic branch.
      
      ```nginx
      server {
          # Standard HTTPS (HTTP/1.1 and HTTP/2)
          listen 443 ssl;
          http2 on;
      
          # HTTP/3 via QUIC (UDP)
          listen 443 quic reuseport;
      
          server_name example.com;
      
          ssl_certificate     /etc/nginx/certs/fullchain.pem;
          ssl_certificate_key /etc/nginx/certs/privkey.pem;
      
          # Required: TLS 1.3 only for QUIC
          ssl_protocols TLSv1.2 TLSv1.3;
      
          # Advertise HTTP/3 support via Alt-Svc header
          add_header Alt-Svc 'h3=":443"; ma=86400' always;
      
          # QUIC-specific settings
          quic_retry on;                 # Enable address validation
          ssl_early_data on;             # Enable 0-RTT (with replay protection)
      
          # Required for QUIC
          ssl_session_tickets on;
      }
      ```
      
      ### Firewall Configuration for QUIC
      
      ```bash
      # Allow UDP port 443 for QUIC
      sudo iptables -A INPUT -p udp --dport 443 -j ACCEPT
      
      # Or with firewalld
      sudo firewall-cmd --permanent --add-port=443/udp
      sudo firewall-cmd --reload
      
      # Or with ufw
      sudo ufw allow 443/udp
      ```
      
      ### HTTP/3 Benefits
      
      | Feature | HTTP/2 (TCP) | HTTP/3 (QUIC) |
      |---------|-------------|---------------|
      | Connection setup | 2-3 RTT (TCP + TLS) | 0-1 RTT |
      | Head-of-line blocking | Yes (TCP level) | No (per-stream) |
      | Connection migration | No (IP changes break) | Yes (connection ID) |
      | Packet loss handling | All streams blocked | Only affected stream |
      | 0-RTT resumption | TLS 1.3 only | Built-in |
      
      ---
      
      ## Connection Draining
      
      ### Graceful Reload
      
      ```bash
      # Graceful reload: new workers start, old workers finish existing requests
      sudo nginx -s reload
      
      # What happens:
      # 1. Master process reads new config
      # 2. Starts new worker processes with new config
      # 3. Old workers stop accepting new connections
      # 4. Old workers finish processing existing requests
      # 5. Old workers exit
      ```
      
      ### Worker Shutdown Timeout
      
      ```nginx
      # Maximum time for old workers to finish requests during reload
      # After this timeout, old workers are forcefully terminated
      worker_shutdown_timeout 30s;
      ```
      
      ### Zero-Downtime Deployment
      
      ```bash
      # 1. Deploy new application code
      # 2. Signal nginx to reload config
      sudo nginx -t && sudo nginx -s reload
      
      # Or with systemd
      sudo nginx -t && sudo systemctl reload nginx
      ```
      
      ### Upstream Draining
      
      ```nginx
      upstream backend {
          server 127.0.0.1:3000;
      
          # Mark server as draining (finish existing, no new)
          server 127.0.0.1:3001 down;    # Use 'down' to stop new traffic
      
          server 127.0.0.1:3002;
      }
      ```
      
      ---
      
      ## Monitoring
      
      ### stub_status Module
      
      ```nginx
      server {
          listen 8080;
      
          # Restrict to internal access
          allow 127.0.0.1;
          allow 10.0.0.0/8;
          deny all;
      
          location /nginx_status {
              stub_status;
          }
      }
      ```
      
      Output:
      
      ```
      Active connections: 291
      server accepts handled requests
       16630948 16630948 31070465
      Reading: 6 Writing: 179 Waiting: 106
      ```
      
      | Metric | Meaning |
      |--------|---------|
      | Active connections | Current active client connections (including waiting) |
      | accepts | Total accepted connections |
      | handled | Total handled connections (should equal accepts) |
      | requests | Total client requests |
      | Reading | Connections where nginx is reading the request header |
      | Writing | Connections where nginx is writing response to client |
      | Waiting | Idle keepalive connections |
      
      ### Request Timing Variables
      
      Use these in log formats for performance monitoring.
      
      ```nginx
      http {
          log_format performance '$remote_addr - $remote_user [$time_local] '
                                 '"$request" $status $body_bytes_sent '
                                 '"$http_referer" "$http_user_agent" '
                                 'rt=$request_time '
                                 'urt=$upstream_response_time '
                                 'uct=$upstream_connect_time '
                                 'uht=$upstream_header_time '
                                 'cs=$upstream_cache_status';
      
          access_log /var/log/nginx/performance.log performance;
      }
      ```
      
      ### Timing Variable Reference
      
      | Variable | Meaning |
      |----------|---------|
      | `$request_time` | Total time from first byte read to last byte sent (seconds, ms resolution) |
      | `$upstream_response_time` | Time from establishing upstream connection to receiving last byte |
      | `$upstream_connect_time` | Time to establish connection to upstream server |
      | `$upstream_header_time` | Time from connection to receiving response headers from upstream |
      | `$upstream_cache_status` | HIT, MISS, BYPASS, EXPIRED, STALE, UPDATING, REVALIDATED |
      
      ### Conditional Logging
      
      ```nginx
      # Only log slow requests (> 1 second)
      map $request_time $loggable_slow {
          ~^[0-9]*\.[0-9]$  0;    # < 1 second
          default            1;    # >= 1 second
      }
      
      access_log /var/log/nginx/slow.log performance if=$loggable_slow;
      
      # Don't log health checks
      map $request_uri $loggable {
          /health     0;
          /ping       0;
          default     1;
      }
      
      access_log /var/log/nginx/access.log combined if=$loggable;
      ```
      
      ### JSON Log Format
      
      Easier to parse with log aggregation tools (ELK, Loki, etc.).
      
      ```nginx
      log_format json_combined escape=json
          '{'
              '"time":"$time_iso8601",'
              '"remote_addr":"$remote_addr",'
              '"request_method":"$request_method",'
              '"request_uri":"$request_uri",'
              '"status":$status,'
              '"body_bytes_sent":$body_bytes_sent,'
              '"request_time":$request_time,'
              '"upstream_response_time":"$upstream_response_time",'
              '"upstream_cache_status":"$upstream_cache_status",'
              '"http_referrer":"$http_referer",'
              '"http_user_agent":"$http_user_agent",'
              '"server_name":"$server_name"'
          '}';
      
      access_log /var/log/nginx/access.json json_combined;
      ```
      
      ### Integration with Prometheus
      
      Use the `nginx-prometheus-exporter` for Prometheus/Grafana monitoring.
      
      ```bash
      # Run nginx-prometheus-exporter
      ./nginx-prometheus-exporter -nginx.scrape-uri=http://127.0.0.1:8080/nginx_status
      ```
      
      Or use the VTS (Virtual Host Traffic Status) module for more detailed metrics:
      
      ```nginx
      # Requires ngx_http_vhost_traffic_status_module
      http {
          vhost_traffic_status_zone;
      
          server {
              listen 8080;
      
              location /status {
                  vhost_traffic_status_display;
                  vhost_traffic_status_display_format prometheus;
              }
          }
      }
      ```
      
      ### Quick Health Check Script
      
      ```bash
      #!/bin/bash
      # nginx-health.sh - Quick nginx health check
      
      NGINX_STATUS="http://127.0.0.1:8080/nginx_status"
      RESPONSE=$(curl -s "$NGINX_STATUS")
      
      ACTIVE=$(echo "$RESPONSE" | rg -o 'Active connections: (\d+)' -r '$1')
      WAITING=$(echo "$RESPONSE" | rg -o 'Waiting: (\d+)' -r '$1')
      READING=$(echo "$RESPONSE" | rg -o 'Reading: (\d+)' -r '$1')
      WRITING=$(echo "$RESPONSE" | rg -o 'Writing: (\d+)' -r '$1')
      
      echo "Active: $ACTIVE | Reading: $READING | Writing: $WRITING | Waiting: $WAITING"
      
      # Alert if active connections exceed threshold
      if [ "$ACTIVE" -gt 5000 ]; then
          echo "WARNING: High connection count: $ACTIVE"
      fi
      ```
      
    • reverse-proxy.md 26.2 KB
      # Reverse Proxy Reference
      
      Comprehensive guide to Nginx reverse proxy configuration: upstream blocks, load balancing algorithms, proxy headers, WebSocket and gRPC proxying, caching, and production-ready configurations.
      
      ---
      
      ## Table of Contents
      
      1. [Upstream Blocks](#upstream-blocks)
      2. [Load Balancing Algorithms](#load-balancing-algorithms)
      3. [Health Checks](#health-checks)
      4. [Proxy Headers](#proxy-headers)
      5. [WebSocket Proxy](#websocket-proxy)
      6. [gRPC Proxy](#grpc-proxy)
      7. [Proxy Caching](#proxy-caching)
      8. [Proxy Buffering](#proxy-buffering)
      9. [Keepalive Connections](#keepalive-connections)
      10. [Timeout Configuration](#timeout-configuration)
      11. [Real-World Configurations](#real-world-configurations)
      
      ---
      
      ## Upstream Blocks
      
      An `upstream` block defines a group of backend servers that Nginx can proxy requests to.
      
      ### Basic Upstream
      
      ```nginx
      upstream backend {
          server 127.0.0.1:3000;
          server 127.0.0.1:3001;
          server 127.0.0.1:3002;
      }
      
      server {
          listen 80;
          location / {
              proxy_pass http://backend;
          }
      }
      ```
      
      ### Server Directive Parameters
      
      ```nginx
      upstream backend {
          # Basic server
          server 127.0.0.1:3000;
      
          # Weighted server - receives 3x traffic
          server 127.0.0.1:3001 weight=3;
      
          # Backup server - only used when all primary servers are down
          server 127.0.0.1:3002 backup;
      
          # Mark server as permanently unavailable
          server 127.0.0.1:3003 down;
      
          # Failure detection: after 3 fails within 30s, mark unavailable for 30s
          server 127.0.0.1:3004 max_fails=3 fail_timeout=30s;
      
          # Limit concurrent connections to this server
          server 127.0.0.1:3005 max_conns=100;
      
          # Unix socket backend
          server unix:/var/run/app.sock;
      
          # Resolve hostname (requires resolver directive)
          server backend.service.consul resolve;
      }
      ```
      
      ### Parameter Reference
      
      | Parameter | Default | Description |
      |-----------|---------|-------------|
      | `weight=N` | 1 | Relative weight for weighted load balancing |
      | `max_fails=N` | 1 | Number of failed attempts before marking unavailable |
      | `fail_timeout=T` | 10s | Time to consider fails AND duration to mark unavailable |
      | `backup` | - | Only used when all non-backup servers are unavailable |
      | `down` | - | Permanently marks server as unavailable |
      | `max_conns=N` | 0 (unlimited) | Maximum concurrent connections to this server |
      | `resolve` | - | Monitor DNS changes and update upstream automatically |
      | `slow_start=T` | 0 | Gradually increase traffic to recovered server (Nginx Plus) |
      
      ---
      
      ## Load Balancing Algorithms
      
      ### Round-Robin (Default)
      
      Distributes requests sequentially across servers. No directive needed.
      
      ```nginx
      upstream backend {
          # Round-robin is the default - no directive needed
          server 127.0.0.1:3000;
          server 127.0.0.1:3001;
          server 127.0.0.1:3002;
      }
      ```
      
      **Use when:** Backends are homogeneous and requests have similar processing time.
      
      ### Weighted Round-Robin
      
      ```nginx
      upstream backend {
          server 127.0.0.1:3000 weight=5;   # Gets 5/8 of requests
          server 127.0.0.1:3001 weight=2;   # Gets 2/8 of requests
          server 127.0.0.1:3002 weight=1;   # Gets 1/8 of requests
      }
      ```
      
      **Use when:** Backends have different capacity (CPU, memory).
      
      ### Least Connections
      
      Routes to the server with the fewest active connections.
      
      ```nginx
      upstream backend {
          least_conn;
          server 127.0.0.1:3000;
          server 127.0.0.1:3001;
          server 127.0.0.1:3002;
      }
      ```
      
      **Use when:** Requests have variable processing time. Prevents slow requests from piling up on one server.
      
      ### IP Hash
      
      Routes requests from the same client IP to the same backend server (session persistence).
      
      ```nginx
      upstream backend {
          ip_hash;
          server 127.0.0.1:3000;
          server 127.0.0.1:3001;
          server 127.0.0.1:3002;
      }
      ```
      
      **Use when:** Application requires sticky sessions and you can't use external session storage.
      
      **Caveat:** If clients are behind a NAT/proxy, many IPs map to one, causing uneven distribution.
      
      ### Random
      
      Randomly selects a server for each request.
      
      ```nginx
      upstream backend {
          random;
          server 127.0.0.1:3000;
          server 127.0.0.1:3001;
          server 127.0.0.1:3002;
      }
      ```
      
      ### Random with Two Choices
      
      Picks two servers at random, then selects the one with fewer connections (power of two choices).
      
      ```nginx
      upstream backend {
          random two least_conn;
          server 127.0.0.1:3000;
          server 127.0.0.1:3001;
          server 127.0.0.1:3002;
      }
      ```
      
      **Use when:** Large number of backends where least_conn coordination overhead is high.
      
      ### Hash (Consistent Hashing)
      
      Map requests to servers based on a configurable key.
      
      ```nginx
      upstream backend {
          hash $request_uri consistent;
          server 127.0.0.1:3000;
          server 127.0.0.1:3001;
          server 127.0.0.1:3002;
      }
      ```
      
      **Use when:** You want cache affinity (same URLs always go to the same backend for better cache hit rates).
      
      The `consistent` parameter uses ketama consistent hashing, which minimizes redistribution when servers are added/removed.
      
      ### Algorithm Comparison
      
      | Algorithm | Session Persistence | Even Distribution | Variable Request Time | Best For |
      |-----------|--------------------|--------------------|----------------------|----------|
      | Round-robin | No | Yes (uniform) | Poor | Homogeneous backends |
      | Weighted | No | Yes (proportional) | Poor | Mixed-capacity backends |
      | Least connections | No | Adapts to load | Good | Variable processing time |
      | IP hash | Yes (by IP) | Depends on IPs | Poor | Sticky sessions |
      | Random two | No | Good | Good | Large clusters |
      | Hash | Yes (by key) | Depends on keys | Poor | Cache affinity |
      
      ---
      
      ## Health Checks
      
      ### Passive Health Checks (Open Source)
      
      Nginx detects unhealthy backends based on failed request attempts.
      
      ```nginx
      upstream backend {
          server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
          server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
          server 127.0.0.1:3002 max_fails=3 fail_timeout=30s;
      }
      ```
      
      **How it works:**
      1. If a server fails `max_fails` times within `fail_timeout` seconds, it's marked unavailable
      2. After `fail_timeout` seconds, Nginx sends one test request
      3. If the test succeeds, the server is marked available again
      
      **What counts as a failure:** Controlled by `proxy_next_upstream`:
      
      ```nginx
      location / {
          proxy_pass http://backend;
      
          # What errors trigger failover to next upstream
          proxy_next_upstream error timeout http_502 http_503 http_504;
      
          # Limit retries across upstream servers
          proxy_next_upstream_tries 3;
      
          # Limit total time for all retries
          proxy_next_upstream_timeout 10s;
      }
      ```
      
      ### Active Health Checks (Nginx Plus or Third-Party)
      
      For open-source Nginx, use the `nginx_upstream_check_module` (third-party) or external health check tools.
      
      ```nginx
      # Nginx Plus active health check
      upstream backend {
          zone backend_zone 64k;    # Required for active checks
      
          server 127.0.0.1:3000;
          server 127.0.0.1:3001;
      }
      
      server {
          location / {
              proxy_pass http://backend;
              health_check interval=5s fails=3 passes=2 uri=/health;
          }
      }
      ```
      
      ### Application Health Endpoint Pattern
      
      ```nginx
      # Backend should implement /health returning 200
      location /health {
          proxy_pass http://backend;
          access_log off;              # Don't clutter logs
          proxy_connect_timeout 2s;    # Fail fast
          proxy_read_timeout 2s;
      }
      ```
      
      ---
      
      ## Proxy Headers
      
      ### Essential Headers
      
      Every reverse proxy should forward these headers so the backend knows about the original request.
      
      ```nginx
      location / {
          proxy_pass http://backend;
      
          # Pass the original Host header
          proxy_set_header Host $host;
      
          # Client's real IP address
          proxy_set_header X-Real-IP $remote_addr;
      
          # Append to existing forwarded-for chain
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      
          # Original protocol (http or https)
          proxy_set_header X-Forwarded-Proto $scheme;
      
          # Original port
          proxy_set_header X-Forwarded-Port $server_port;
      }
      ```
      
      ### Header Reference
      
      | Header | Variable | Purpose |
      |--------|----------|---------|
      | `Host` | `$host` | Original hostname from client request |
      | `X-Real-IP` | `$remote_addr` | Client's IP address |
      | `X-Forwarded-For` | `$proxy_add_x_forwarded_for` | Chain of proxy IPs |
      | `X-Forwarded-Proto` | `$scheme` | Original protocol (http/https) |
      | `X-Forwarded-Port` | `$server_port` | Original port number |
      | `X-Request-ID` | `$request_id` | Unique request identifier for tracing |
      | `Connection` | `""` | Clear hop-by-hop header for keepalive |
      
      ### Reusable Headers Include
      
      Create a shared include file to avoid repetition:
      
      ```nginx
      # /etc/nginx/includes/proxy-headers.conf
      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_set_header X-Forwarded-Port $server_port;
      proxy_set_header X-Request-ID $request_id;
      ```
      
      ```nginx
      # Usage in server/location blocks
      location / {
          proxy_pass http://backend;
          include /etc/nginx/includes/proxy-headers.conf;
      }
      ```
      
      ### Hiding Backend Headers
      
      ```nginx
      location / {
          proxy_pass http://backend;
      
          # Remove headers that leak backend info
          proxy_hide_header X-Powered-By;
          proxy_hide_header Server;
          proxy_hide_header X-AspNet-Version;
      
          # Pass through headers that are hidden by default
          proxy_pass_header X-Custom-Header;
      }
      ```
      
      ---
      
      ## WebSocket Proxy
      
      WebSocket requires HTTP/1.1 with the Upgrade mechanism.
      
      ### Basic WebSocket Proxy
      
      ```nginx
      # Map to handle Upgrade header
      map $http_upgrade $connection_upgrade {
          default upgrade;
          ''      close;
      }
      
      server {
          listen 80;
          server_name ws.example.com;
      
          location /ws/ {
              proxy_pass http://127.0.0.1:3000;
              proxy_http_version 1.1;
      
              # WebSocket upgrade headers
              proxy_set_header Upgrade $http_upgrade;
              proxy_set_header Connection $connection_upgrade;
      
              # Standard proxy headers
              proxy_set_header Host $host;
              proxy_set_header X-Real-IP $remote_addr;
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      
              # Long timeouts for persistent connections
              proxy_read_timeout 3600s;
              proxy_send_timeout 3600s;
          }
      }
      ```
      
      ### WebSocket with SSL
      
      ```nginx
      server {
          listen 443 ssl http2;
          server_name ws.example.com;
      
          ssl_certificate     /etc/nginx/certs/fullchain.pem;
          ssl_certificate_key /etc/nginx/certs/privkey.pem;
      
          location /ws/ {
              proxy_pass http://127.0.0.1:3000;
              proxy_http_version 1.1;
              proxy_set_header Upgrade $http_upgrade;
              proxy_set_header Connection $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;
              proxy_read_timeout 3600s;
              proxy_send_timeout 3600s;
          }
      }
      ```
      
      ### Socket.IO Configuration
      
      Socket.IO uses both WebSocket and HTTP long-polling:
      
      ```nginx
      location /socket.io/ {
          proxy_pass http://127.0.0.1:3000;
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection $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;
      
          # Important for Socket.IO long-polling fallback
          proxy_buffering off;
          proxy_cache off;
      }
      ```
      
      ---
      
      ## gRPC Proxy
      
      ### Basic gRPC Proxy
      
      ```nginx
      upstream grpc_backend {
          server 127.0.0.1:50051;
      }
      
      server {
          listen 443 ssl http2;
          server_name grpc.example.com;
      
          ssl_certificate     /etc/nginx/certs/fullchain.pem;
          ssl_certificate_key /etc/nginx/certs/privkey.pem;
      
          location / {
              # Use grpc_pass for gRPC backends
              grpc_pass grpc://grpc_backend;
      
              # gRPC-specific headers
              grpc_set_header X-Real-IP $remote_addr;
              grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          }
      }
      ```
      
      ### gRPC with TLS to Backend
      
      ```nginx
      location / {
          # grpcs:// for TLS-encrypted gRPC backends
          grpc_pass grpcs://grpc_backend;
      
          grpc_ssl_certificate     /etc/nginx/certs/client.pem;
          grpc_ssl_certificate_key /etc/nginx/certs/client.key;
          grpc_ssl_verify on;
          grpc_ssl_trusted_certificate /etc/nginx/certs/ca.pem;
      }
      ```
      
      ### gRPC Error Handling
      
      ```nginx
      location / {
          grpc_pass grpc://grpc_backend;
      
          # Intercept gRPC errors and return custom responses
          grpc_intercept_errors on;
      
          error_page 502 = /error502grpc;
      }
      
      location = /error502grpc {
          internal;
          default_type application/grpc;
          add_header grpc-status 14;
          add_header grpc-message "Backend unavailable";
          return 204;
      }
      ```
      
      ---
      
      ## Proxy Caching
      
      ### Basic Cache Configuration
      
      ```nginx
      http {
          # Define cache storage
          # levels=1:2       - Two-level directory hierarchy
          # keys_zone=cache:10m - 10MB shared memory for cache keys (~80,000 keys)
          # max_size=10g     - Maximum cache size on disk
          # inactive=60m     - Remove items not accessed in 60 minutes
          # use_temp_path=off - Write directly to cache dir (better performance)
          proxy_cache_path /var/cache/nginx
              levels=1:2
              keys_zone=app_cache:10m
              max_size=10g
              inactive=60m
              use_temp_path=off;
      
          server {
              listen 80;
      
              location / {
                  proxy_pass http://backend;
      
                  # Enable caching with the named zone
                  proxy_cache app_cache;
      
                  # Cache different status codes for different durations
                  proxy_cache_valid 200 302 10m;
                  proxy_cache_valid 404     1m;
                  proxy_cache_valid any     5m;
      
                  # Custom cache key
                  proxy_cache_key "$scheme$request_method$host$request_uri";
      
                  # Add header to show cache status (HIT, MISS, BYPASS, etc.)
                  add_header X-Cache-Status $upstream_cache_status;
              }
          }
      }
      ```
      
      ### Cache Bypass
      
      ```nginx
      location / {
          proxy_pass http://backend;
          proxy_cache app_cache;
          proxy_cache_valid 200 10m;
      
          # Bypass cache when specific conditions are met
          proxy_cache_bypass $http_cache_control;   # Client sends Cache-Control
          proxy_cache_bypass $cookie_nocache;       # Cookie "nocache" is set
          proxy_cache_bypass $arg_nocache;          # Query param ?nocache=1
      
          # Don't store in cache under these conditions
          proxy_no_cache $http_pragma;              # Client sends Pragma: no-cache
          proxy_no_cache $arg_nocache;
      }
      ```
      
      ### Stale Cache (Serve Old Content During Errors)
      
      ```nginx
      location / {
          proxy_pass http://backend;
          proxy_cache app_cache;
          proxy_cache_valid 200 10m;
      
          # Serve stale content when backend is down or slow
          proxy_cache_use_stale error timeout updating
                                http_500 http_502 http_503 http_504;
      
          # Update cache in background while serving stale
          proxy_cache_background_update on;
      
          # Only one request refreshes cache, others get stale
          proxy_cache_lock on;
          proxy_cache_lock_timeout 5s;
      }
      ```
      
      ### Cache Purge
      
      ```nginx
      # Requires ngx_cache_purge module
      location ~ /purge(/.*) {
          allow 127.0.0.1;
          deny all;
          proxy_cache_purge app_cache "$scheme$request_method$host$1";
      }
      ```
      
      Usage: `curl -X PURGE https://example.com/purge/api/data`
      
      ---
      
      ## Proxy Buffering
      
      ### Buffering On (Default)
      
      Nginx reads the entire response from the backend, then sends it to the client. Good for fast backends with slow clients.
      
      ```nginx
      location / {
          proxy_pass http://backend;
      
          # Buffering on (default)
          proxy_buffering on;
      
          # Size of the buffer for the first part of the response (headers)
          proxy_buffer_size 8k;
      
          # Number and size of buffers for the response body
          proxy_buffers 8 16k;
      
          # Maximum size that can be busy sending to client
          proxy_busy_buffers_size 32k;
      
          # Temporary files if response exceeds buffers
          proxy_temp_file_write_size 64k;
          proxy_max_temp_file_size 1024m;
      }
      ```
      
      ### Buffering Off
      
      Send data to client as soon as it arrives from backend. Required for streaming.
      
      ```nginx
      location /stream/ {
          proxy_pass http://backend;
      
          # Disable buffering for streaming responses
          proxy_buffering off;
      
          # Also disable request body buffering
          proxy_request_buffering off;
      }
      ```
      
      **Disable buffering for:**
      - Server-Sent Events (SSE)
      - Long-polling
      - Streaming downloads
      - Real-time data feeds
      - Large file downloads where you want immediate start
      
      ### Server-Sent Events (SSE) Configuration
      
      ```nginx
      location /events/ {
          proxy_pass http://backend;
          proxy_http_version 1.1;
      
          # Critical for SSE
          proxy_buffering off;
          proxy_cache off;
      
          # Don't add compression (breaks streaming)
          proxy_set_header Accept-Encoding "";
      
          # Keep connection alive
          proxy_set_header Connection "";
          proxy_read_timeout 86400s;
      
          # Chunked transfer
          chunked_transfer_encoding on;
      }
      ```
      
      ---
      
      ## Keepalive Connections
      
      Reuse connections to upstream servers instead of opening a new TCP connection per request.
      
      ### Upstream Keepalive
      
      ```nginx
      upstream backend {
          server 127.0.0.1:3000;
          server 127.0.0.1:3001;
      
          # Keep up to 32 idle connections alive per worker process
          keepalive 32;
      
          # Maximum requests per keepalive connection before closing
          keepalive_requests 1000;
      
          # Idle timeout for keepalive connections
          keepalive_timeout 60s;
      }
      
      server {
          location / {
              proxy_pass http://backend;
      
              # Required for keepalive to work with upstream
              proxy_http_version 1.1;
              proxy_set_header Connection "";
      
              # Standard headers
              proxy_set_header Host $host;
              proxy_set_header X-Real-IP $remote_addr;
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          }
      }
      ```
      
      **Important:** `proxy_http_version 1.1` and `proxy_set_header Connection ""` are both required. HTTP/1.0 uses `Connection: close` by default, which prevents keepalive.
      
      ### Keepalive Sizing
      
      | Scenario | `keepalive` Value | Rationale |
      |----------|-------------------|-----------|
      | Low traffic | 8-16 | Minimal idle connections |
      | Medium traffic | 32-64 | Balance memory vs connection reuse |
      | High traffic | 128-256 | Maximize connection reuse |
      | Microservices | 16-32 per upstream | Per-service pools |
      
      ---
      
      ## Timeout Configuration
      
      ### Complete Timeout Reference
      
      ```nginx
      location / {
          proxy_pass http://backend;
      
          # Time to establish connection to backend
          proxy_connect_timeout 5s;    # Default: 60s
      
          # Time to wait for backend to start sending response
          proxy_read_timeout 60s;      # Default: 60s
      
          # Time allowed to send request body to backend
          proxy_send_timeout 60s;      # Default: 60s
      }
      ```
      
      ### Timeout Guidelines
      
      | Timeout | Typical Value | Use Case |
      |---------|---------------|----------|
      | `proxy_connect_timeout` | 3-5s | Fail fast if backend is unreachable |
      | `proxy_read_timeout` | 30-60s | API responses, page rendering |
      | `proxy_read_timeout` | 300s+ | File uploads, long reports |
      | `proxy_read_timeout` | 3600s | WebSocket, SSE |
      | `proxy_send_timeout` | 30-60s | Most applications |
      
      ### Per-Location Timeouts
      
      ```nginx
      # Fast API endpoint
      location /api/ {
          proxy_pass http://backend;
          proxy_connect_timeout 3s;
          proxy_read_timeout 10s;
      }
      
      # File upload endpoint
      location /upload/ {
          proxy_pass http://backend;
          proxy_connect_timeout 5s;
          proxy_read_timeout 300s;
          client_max_body_size 100m;
      }
      
      # Report generation (slow)
      location /reports/ {
          proxy_pass http://backend;
          proxy_connect_timeout 5s;
          proxy_read_timeout 600s;
      }
      ```
      
      ---
      
      ## Real-World Configurations
      
      ### Node.js Application
      
      ```nginx
      upstream nodejs {
          server 127.0.0.1:3000;
          keepalive 32;
      }
      
      server {
          listen 443 ssl http2;
          server_name app.example.com;
      
          ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
          include /etc/nginx/includes/ssl-params.conf;
      
          # Security headers
          add_header X-Content-Type-Options nosniff always;
          add_header X-Frame-Options DENY always;
          add_header Referrer-Policy strict-origin-when-cross-origin always;
      
          # Proxy to Node.js
          location / {
              proxy_pass http://nodejs;
              proxy_http_version 1.1;
              proxy_set_header Connection "";
      
              include /etc/nginx/includes/proxy-headers.conf;
      
              proxy_connect_timeout 5s;
              proxy_read_timeout 60s;
      
              # Handle large JWT tokens
              proxy_buffer_size 16k;
              proxy_buffers 4 32k;
          }
      
          # Serve static files directly (bypass Node.js)
          location /static/ {
              alias /var/www/app/public/;
              expires 30d;
              add_header Cache-Control "public, immutable";
              access_log off;
          }
      
          # WebSocket endpoint
          location /ws {
              proxy_pass http://nodejs;
              proxy_http_version 1.1;
              proxy_set_header Upgrade $http_upgrade;
              proxy_set_header Connection $connection_upgrade;
              proxy_set_header Host $host;
              proxy_set_header X-Real-IP $remote_addr;
              proxy_read_timeout 3600s;
          }
      }
      ```
      
      ### Python/Gunicorn Application
      
      ```nginx
      upstream gunicorn {
          server unix:/run/gunicorn/app.sock fail_timeout=10s;
          keepalive 16;
      }
      
      server {
          listen 443 ssl http2;
          server_name api.example.com;
      
          ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
          include /etc/nginx/includes/ssl-params.conf;
      
          client_max_body_size 10m;
      
          location / {
              proxy_pass http://gunicorn;
              proxy_http_version 1.1;
              proxy_set_header Connection "";
      
              include /etc/nginx/includes/proxy-headers.conf;
      
              proxy_connect_timeout 5s;
              proxy_read_timeout 30s;
      
              proxy_buffer_size 8k;
              proxy_buffers 4 16k;
          }
      
          # Django static files
          location /static/ {
              alias /var/www/app/staticfiles/;
              expires 30d;
              access_log off;
          }
      
          # Django media uploads
          location /media/ {
              alias /var/www/app/media/;
              expires 7d;
          }
      }
      ```
      
      ### Go Binary Application
      
      ```nginx
      upstream goapp {
          server 127.0.0.1:8080;
          server 127.0.0.1:8081;
          keepalive 64;
      }
      
      server {
          listen 443 ssl http2;
          server_name service.example.com;
      
          ssl_certificate     /etc/letsencrypt/live/service.example.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/service.example.com/privkey.pem;
          include /etc/nginx/includes/ssl-params.conf;
      
          # Go apps typically serve their own static files
          location / {
              proxy_pass http://goapp;
              proxy_http_version 1.1;
              proxy_set_header Connection "";
      
              include /etc/nginx/includes/proxy-headers.conf;
      
              proxy_connect_timeout 3s;
              proxy_read_timeout 30s;
      
              # Go apps handle large payloads efficiently
              proxy_request_buffering off;
          }
      
          # Health check
          location /healthz {
              proxy_pass http://goapp;
              access_log off;
              proxy_connect_timeout 2s;
              proxy_read_timeout 2s;
          }
      }
      ```
      
      ### PHP-FPM Application
      
      ```nginx
      upstream php-fpm {
          server unix:/run/php/php8.3-fpm.sock;
      }
      
      server {
          listen 443 ssl http2;
          server_name site.example.com;
      
          ssl_certificate     /etc/letsencrypt/live/site.example.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/site.example.com/privkey.pem;
          include /etc/nginx/includes/ssl-params.conf;
      
          root /var/www/site/public;
          index index.php index.html;
      
          client_max_body_size 50m;
      
          # Try static file first, then directory, then PHP
          location / {
              try_files $uri $uri/ /index.php?$query_string;
          }
      
          # PHP processing
          location ~ \.php$ {
              fastcgi_pass php-fpm;
              fastcgi_index index.php;
              fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
              include fastcgi_params;
      
              fastcgi_connect_timeout 5s;
              fastcgi_send_timeout 30s;
              fastcgi_read_timeout 30s;
      
              fastcgi_buffer_size 16k;
              fastcgi_buffers 4 16k;
          }
      
          # Deny access to hidden files (except .well-known)
          location ~ /\.(?!well-known) {
              deny all;
          }
      
          # Static assets
          location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
              expires 30d;
              add_header Cache-Control "public, immutable";
              access_log off;
          }
      }
      ```
      
      ### Multiple Services on One Domain (Path-Based Routing)
      
      ```nginx
      # Upstream definitions for each service
      upstream api_service {
          server 127.0.0.1:3000;
          keepalive 32;
      }
      
      upstream admin_service {
          server 127.0.0.1:4000;
          keepalive 16;
      }
      
      upstream docs_service {
          server 127.0.0.1:5000;
          keepalive 8;
      }
      
      server {
          listen 443 ssl http2;
          server_name example.com;
      
          ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
          include /etc/nginx/includes/ssl-params.conf;
      
          # API service at /api/
          location /api/ {
              proxy_pass http://api_service/;    # Trailing / strips /api/ prefix
              proxy_http_version 1.1;
              proxy_set_header Connection "";
              include /etc/nginx/includes/proxy-headers.conf;
      
              # API-specific settings
              proxy_read_timeout 30s;
              client_max_body_size 10m;
      
              # Rate limiting for API
              limit_req zone=api burst=20 nodelay;
          }
      
          # Admin panel at /admin/
          location /admin/ {
              proxy_pass http://admin_service/;
              proxy_http_version 1.1;
              proxy_set_header Connection "";
              include /etc/nginx/includes/proxy-headers.conf;
      
              # Restrict admin access by IP
              allow 10.0.0.0/8;
              allow 192.168.0.0/16;
              deny all;
          }
      
          # Documentation at /docs/
          location /docs/ {
              proxy_pass http://docs_service/;
              proxy_http_version 1.1;
              proxy_set_header Connection "";
              include /etc/nginx/includes/proxy-headers.conf;
      
              # Cache documentation pages
              proxy_cache app_cache;
              proxy_cache_valid 200 1h;
          }
      
          # Frontend SPA (catch-all)
          location / {
              root /var/www/frontend/dist;
              index index.html;
              try_files $uri $uri/ /index.html;
          }
      }
      ```
      
      ### Proxy to Multiple Ports with Subdomain Routing
      
      ```nginx
      # Alternative: subdomain-based routing
      server {
          listen 443 ssl http2;
          server_name api.example.com;
          include /etc/nginx/includes/ssl-params.conf;
      
          location / {
              proxy_pass http://api_service;
              include /etc/nginx/includes/proxy-headers.conf;
          }
      }
      
      server {
          listen 443 ssl http2;
          server_name admin.example.com;
          include /etc/nginx/includes/ssl-params.conf;
      
          location / {
              proxy_pass http://admin_service;
              include /etc/nginx/includes/proxy-headers.conf;
          }
      }
      ```
      
    • ssl-security.md 24 KB
      # SSL/TLS & Security Reference
      
      Comprehensive guide to Nginx SSL/TLS configuration, Let's Encrypt automation, security headers, rate limiting, access control, and mutual TLS.
      
      ---
      
      ## Table of Contents
      
      1. [TLS Configuration](#tls-configuration)
      2. [Let's Encrypt & Certbot](#lets-encrypt--certbot)
      3. [Certificate Management](#certificate-management)
      4. [HSTS](#hsts)
      5. [OCSP Stapling](#ocsp-stapling)
      6. [Security Headers](#security-headers)
      7. [Rate Limiting](#rate-limiting)
      8. [IP Restrictions](#ip-restrictions)
      9. [Basic Authentication](#basic-authentication)
      10. [Mutual TLS (mTLS)](#mutual-tls-mtls)
      11. [HTTP to HTTPS Redirect](#http-to-https-redirect)
      
      ---
      
      ## TLS Configuration
      
      ### Modern Configuration (TLS 1.3 Only)
      
      For services where all clients support TLS 1.3 (modern browsers, API clients you control).
      
      ```nginx
      server {
          listen 443 ssl http2;
          server_name example.com;
      
          ssl_certificate     /etc/nginx/certs/fullchain.pem;
          ssl_certificate_key /etc/nginx/certs/privkey.pem;
      
          # TLS 1.3 only
          ssl_protocols TLSv1.3;
      
          # TLS 1.3 ciphers are not configurable via ssl_ciphers
          # They are negotiated automatically:
          # TLS_AES_256_GCM_SHA384
          # TLS_CHACHA20_POLY1305_SHA256
          # TLS_AES_128_GCM_SHA256
      
          ssl_prefer_server_ciphers off;
      }
      ```
      
      ### Intermediate Configuration (TLS 1.2 + 1.3)
      
      Recommended for most production sites. Compatible with all modern browsers.
      
      ```nginx
      server {
          listen 443 ssl http2;
          server_name example.com;
      
          ssl_certificate     /etc/nginx/certs/fullchain.pem;
          ssl_certificate_key /etc/nginx/certs/privkey.pem;
      
          # TLS 1.2 and 1.3
          ssl_protocols TLSv1.2 TLSv1.3;
      
          # Cipher suite for TLS 1.2 (TLS 1.3 ciphers are automatic)
          ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
          ssl_prefer_server_ciphers off;
      
          # DH parameters for DHE ciphers
          ssl_dhparam /etc/nginx/dhparam.pem;
      
          # Session caching
          ssl_session_cache shared:SSL:10m;
          ssl_session_timeout 1d;
          ssl_session_tickets off;
      }
      ```
      
      ### Generate DH Parameters
      
      ```bash
      # Generate 4096-bit DH parameters (takes several minutes)
      openssl dhparam -out /etc/nginx/dhparam.pem 4096
      
      # Or use pre-generated params from Mozilla (faster, still secure)
      curl -sL https://ssl-config.mozilla.org/ffdhe2048.txt > /etc/nginx/dhparam.pem
      ```
      
      ### SSL Session Configuration
      
      ```nginx
      # Shared session cache across all worker processes
      # 10m = 10MB, enough for ~40,000 sessions
      ssl_session_cache shared:SSL:10m;
      
      # Session lifetime
      ssl_session_timeout 1d;
      
      # Disable session tickets (better forward secrecy)
      # Enable only if you rotate ticket keys regularly
      ssl_session_tickets off;
      ```
      
      ### TLS Version Comparison
      
      | Version | Status | Performance | Security | Support |
      |---------|--------|-------------|----------|---------|
      | TLS 1.0 | Deprecated | Slow | Weak | Drop immediately |
      | TLS 1.1 | Deprecated | Slow | Weak | Drop immediately |
      | TLS 1.2 | Active | Good | Strong | All modern browsers |
      | TLS 1.3 | Preferred | Best (0-RTT) | Strongest | 95%+ browsers |
      
      ---
      
      ## Let's Encrypt & Certbot
      
      ### Installation
      
      ```bash
      # Debian/Ubuntu
      sudo apt update
      sudo apt install certbot python3-certbot-nginx
      
      # RHEL/Fedora
      sudo dnf install certbot python3-certbot-nginx
      
      # Alpine
      sudo apk add certbot certbot-nginx
      
      # Snap (universal)
      sudo snap install --classic certbot
      sudo ln -s /snap/bin/certbot /usr/bin/certbot
      ```
      
      ### Obtaining Certificates
      
      #### Nginx Plugin (Easiest)
      
      Certbot automatically modifies your nginx config.
      
      ```bash
      # Single domain
      sudo certbot --nginx -d example.com
      
      # Multiple domains
      sudo certbot --nginx -d example.com -d www.example.com -d api.example.com
      
      # Non-interactive (for automation)
      sudo certbot --nginx --non-interactive --agree-tos \
          --email admin@example.com -d example.com
      ```
      
      #### Webroot Method (No Restart)
      
      Use when you don't want certbot to modify your nginx config.
      
      ```nginx
      # Add this to your nginx server block first
      location /.well-known/acme-challenge/ {
          root /var/www/certbot;
      }
      ```
      
      ```bash
      sudo certbot certonly --webroot -w /var/www/certbot -d example.com
      ```
      
      #### Standalone Method
      
      Certbot runs its own temporary web server (requires port 80 to be free).
      
      ```bash
      # Stop nginx first
      sudo systemctl stop nginx
      
      sudo certbot certonly --standalone -d example.com
      
      # Restart nginx
      sudo systemctl start nginx
      ```
      
      #### DNS Challenge (Wildcard Certificates)
      
      Required for wildcard certificates (`*.example.com`).
      
      ```bash
      # Manual DNS challenge
      sudo certbot certonly --manual --preferred-challenges dns -d "*.example.com"
      
      # With DNS plugin (Cloudflare example)
      sudo certbot certonly --dns-cloudflare \
          --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
          -d example.com -d "*.example.com"
      ```
      
      Cloudflare credentials file:
      
      ```ini
      # /etc/letsencrypt/cloudflare.ini
      dns_cloudflare_api_token = your-api-token-here
      ```
      
      ```bash
      # Secure the credentials file
      sudo chmod 600 /etc/letsencrypt/cloudflare.ini
      ```
      
      ### Auto-Renewal
      
      #### Systemd Timer (Recommended)
      
      Certbot usually installs this automatically.
      
      ```ini
      # /etc/systemd/system/certbot.timer
      [Unit]
      Description=Run certbot twice daily
      
      [Timer]
      OnCalendar=*-*-* 00,12:00:00
      RandomizedDelaySec=3600
      Persistent=true
      
      [Install]
      WantedBy=timers.target
      ```
      
      ```ini
      # /etc/systemd/system/certbot.service
      [Unit]
      Description=Certbot renewal
      
      [Service]
      Type=oneshot
      ExecStart=/usr/bin/certbot renew --quiet
      ```
      
      ```bash
      # Enable and start
      sudo systemctl enable certbot.timer
      sudo systemctl start certbot.timer
      
      # Check status
      sudo systemctl list-timers certbot.timer
      ```
      
      #### Cron Alternative
      
      ```bash
      # /etc/cron.d/certbot
      0 0,12 * * * root certbot renew --quiet --deploy-hook "systemctl reload nginx"
      ```
      
      ### Renewal Hooks
      
      ```bash
      # Test renewal with hooks
      sudo certbot renew --dry-run \
          --pre-hook "echo 'Before renewal'" \
          --post-hook "systemctl reload nginx" \
          --deploy-hook "echo 'Certificate renewed'"
      
      # Hook scripts (placed in /etc/letsencrypt/renewal-hooks/)
      # /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
      #!/bin/bash
      systemctl reload nginx
      ```
      
      ```bash
      chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
      ```
      
      ### Certificate File Locations
      
      ```
      /etc/letsencrypt/live/example.com/
      ├── cert.pem          # Domain certificate only
      ├── chain.pem         # Intermediate CA certificate(s)
      ├── fullchain.pem     # cert.pem + chain.pem (use this for ssl_certificate)
      ├── privkey.pem       # Private key (use this for ssl_certificate_key)
      └── README
      ```
      
      ---
      
      ## Certificate Management
      
      ### Certificate Chain Configuration
      
      ```nginx
      # fullchain.pem includes: domain cert + intermediate CA cert(s)
      ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
      ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
      
      # Trusted certificate for OCSP stapling verification
      ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
      ```
      
      ### Verify Certificate Chain
      
      ```bash
      # Check certificate details
      openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -text -noout
      
      # Verify chain
      openssl verify -CAfile /etc/letsencrypt/live/example.com/chain.pem \
          /etc/letsencrypt/live/example.com/cert.pem
      
      # Check expiration
      openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -enddate
      
      # Test SSL from outside
      openssl s_client -connect example.com:443 -servername example.com
      ```
      
      ### Multiple Certificates (RSA + ECDSA)
      
      Serve different certificate types for maximum compatibility and performance.
      
      ```nginx
      server {
          listen 443 ssl http2;
          server_name example.com;
      
          # RSA certificate (compatibility)
          ssl_certificate     /etc/nginx/certs/example.com-rsa.pem;
          ssl_certificate_key /etc/nginx/certs/example.com-rsa.key;
      
          # ECDSA certificate (performance) - Nginx picks the best one
          ssl_certificate     /etc/nginx/certs/example.com-ecdsa.pem;
          ssl_certificate_key /etc/nginx/certs/example.com-ecdsa.key;
      }
      ```
      
      ---
      
      ## HSTS
      
      HTTP Strict Transport Security tells browsers to always use HTTPS for this domain.
      
      ### Basic HSTS
      
      ```nginx
      # 2-year max-age (recommended for production)
      add_header Strict-Transport-Security "max-age=63072000" always;
      ```
      
      ### HSTS with Subdomains
      
      ```nginx
      # Apply to all subdomains as well
      add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
      ```
      
      ### HSTS Preload
      
      Submit to browser preload list (permanently enforced, difficult to undo).
      
      ```nginx
      add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
      ```
      
      **Before enabling preload:**
      1. Ensure ALL subdomains support HTTPS
      2. Start with a short `max-age` (e.g., 300) and test
      3. Submit at https://hstspreload.org/
      
      ### Gradual HSTS Rollout
      
      ```nginx
      # Step 1: Short max-age, monitor for issues (1 week)
      add_header Strict-Transport-Security "max-age=604800" always;
      
      # Step 2: Increase to 1 month
      add_header Strict-Transport-Security "max-age=2592000" always;
      
      # Step 3: Include subdomains
      add_header Strict-Transport-Security "max-age=2592000; includeSubDomains" always;
      
      # Step 4: Full production (2 years + preload)
      add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
      ```
      
      ---
      
      ## OCSP Stapling
      
      OCSP stapling embeds the certificate's revocation status in the TLS handshake, improving connection speed and privacy.
      
      ```nginx
      server {
          listen 443 ssl http2;
          server_name example.com;
      
          # Enable OCSP stapling
          ssl_stapling on;
      
          # Verify OCSP response using trusted CA cert
          ssl_stapling_verify on;
      
          # CA cert chain for verification (intermediate + root)
          ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
      
          # DNS resolver for OCSP responder lookup
          resolver 1.1.1.1 8.8.8.8 valid=300s;
          resolver_timeout 5s;
      }
      ```
      
      ### Verify OCSP Stapling
      
      ```bash
      # Test OCSP stapling
      openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null | \
          grep -A 17 "OCSP Response Status"
      
      # Should show: "OCSP Response Status: successful (0x0)"
      ```
      
      ---
      
      ## Security Headers
      
      ### Complete Security Headers Configuration
      
      ```nginx
      # /etc/nginx/includes/security-headers.conf
      
      # Prevent MIME type sniffing
      add_header X-Content-Type-Options nosniff always;
      
      # Clickjacking protection
      add_header X-Frame-Options DENY always;
      # Or allow same-origin framing:
      # add_header X-Frame-Options SAMEORIGIN always;
      
      # XSS Protection (legacy browsers)
      add_header X-XSS-Protection "1; mode=block" always;
      
      # Referrer Policy
      add_header Referrer-Policy strict-origin-when-cross-origin always;
      
      # Permissions Policy (formerly Feature-Policy)
      add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
      
      # Content Security Policy
      add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
      
      # Cross-Origin policies
      add_header Cross-Origin-Opener-Policy same-origin always;
      add_header Cross-Origin-Resource-Policy same-origin always;
      add_header Cross-Origin-Embedder-Policy require-corp always;
      ```
      
      ### Usage
      
      ```nginx
      server {
          listen 443 ssl http2;
          server_name example.com;
      
          include /etc/nginx/includes/security-headers.conf;
      
          # ... rest of config
      }
      ```
      
      ### Header Reference
      
      | Header | Value | Purpose |
      |--------|-------|---------|
      | `X-Content-Type-Options` | `nosniff` | Prevent MIME type sniffing |
      | `X-Frame-Options` | `DENY` or `SAMEORIGIN` | Prevent clickjacking |
      | `X-XSS-Protection` | `1; mode=block` | Legacy XSS filter |
      | `Referrer-Policy` | `strict-origin-when-cross-origin` | Control referrer leakage |
      | `Permissions-Policy` | `camera=(), ...` | Disable browser features |
      | `Content-Security-Policy` | `default-src 'self'; ...` | Control resource loading |
      | `Strict-Transport-Security` | `max-age=63072000; ...` | Force HTTPS |
      | `Cross-Origin-Opener-Policy` | `same-origin` | Isolate browsing context |
      | `Cross-Origin-Resource-Policy` | `same-origin` | Prevent cross-origin reads |
      
      ### Content-Security-Policy Examples
      
      ```nginx
      # Minimal CSP (strict)
      add_header Content-Security-Policy "default-src 'self';" always;
      
      # With Google Fonts and Analytics
      add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://www.google-analytics.com;" always;
      
      # API-only (no HTML rendering)
      add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none';" always;
      
      # Report-only mode (for testing)
      add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report;" always;
      ```
      
      ---
      
      ## Rate Limiting
      
      ### Basic Rate Limiting
      
      ```nginx
      http {
          # Define rate limit zone
          # $binary_remote_addr = client IP (compact binary, 4 or 16 bytes)
          # zone=name:size     = shared memory zone name and size
          # rate=10r/s         = 10 requests per second
          limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
      
          server {
              location / {
                  # Apply rate limit
                  # burst=20  = allow 20 excess requests to queue
                  # nodelay   = process burst immediately (don't throttle)
                  limit_req zone=general burst=20 nodelay;
      
                  # Custom status code (default is 503)
                  limit_req_status 429;
      
                  proxy_pass http://backend;
              }
          }
      }
      ```
      
      ### Multiple Rate Limit Zones
      
      ```nginx
      http {
          # Global rate limit: 30 req/s per IP
          limit_req_zone $binary_remote_addr zone=global:10m rate=30r/s;
      
          # Login rate limit: 5 req/min per IP
          limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
      
          # API rate limit: by API key
          limit_req_zone $http_x_api_key zone=api:10m rate=100r/s;
      
          server {
              # Global limit applies everywhere
              limit_req zone=global burst=50 nodelay;
      
              location /api/login {
                  # Stricter limit for login endpoint
                  limit_req zone=login burst=3 nodelay;
                  proxy_pass http://backend;
              }
      
              location /api/ {
                  # API key-based limiting
                  limit_req zone=api burst=200 nodelay;
                  proxy_pass http://backend;
              }
          }
      }
      ```
      
      ### Connection Limiting
      
      Limit the number of simultaneous connections per IP.
      
      ```nginx
      http {
          # Define connection limit zone
          limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
      
          server {
              # Max 20 simultaneous connections per IP
              limit_conn conn_limit 20;
      
              # Limit bandwidth per connection (useful for downloads)
              limit_rate 1m;               # 1MB/s per connection
              limit_rate_after 10m;        # Full speed for first 10MB
      
              location /downloads/ {
                  # Tighter limits for download section
                  limit_conn conn_limit 5;
                  limit_rate 500k;
              }
          }
      }
      ```
      
      ### Rate Limiting with Whitelisting
      
      ```nginx
      http {
          # Map to identify whitelisted IPs
          geo $rate_limit {
              default         1;
              10.0.0.0/8      0;    # Internal network
              192.168.0.0/16  0;    # Private network
              203.0.113.50    0;    # Monitoring server
          }
      
          # Only apply rate limiting to non-whitelisted IPs
          map $rate_limit $rate_limit_key {
              0 "";
              1 $binary_remote_addr;
          }
      
          limit_req_zone $rate_limit_key zone=api:10m rate=10r/s;
      
          server {
              location /api/ {
                  limit_req zone=api burst=20 nodelay;
                  proxy_pass http://backend;
              }
          }
      }
      ```
      
      ### Logging Rate-Limited Requests
      
      ```nginx
      http {
          limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
      
          # Log rate-limited requests at warn level
          limit_req_log_level warn;
      
          # Custom log format for rate-limited requests
          log_format ratelimit '$remote_addr - $remote_user [$time_local] '
                               '"$request" $status $body_bytes_sent '
                               '"limit_req_status=$limit_req_status"';
      }
      ```
      
      ---
      
      ## IP Restrictions
      
      ### Allow/Deny Directives
      
      ```nginx
      location /admin/ {
          # Allow specific IPs and ranges
          allow 10.0.0.0/8;
          allow 192.168.1.0/24;
          allow 203.0.113.50;
      
          # Deny everything else
          deny all;
      
          proxy_pass http://admin_backend;
      }
      ```
      
      **Order matters:** Nginx evaluates `allow`/`deny` rules in order and uses the first match.
      
      ### Geo Module
      
      Map client IP to a variable for conditional logic.
      
      ```nginx
      http {
          geo $allowed_country {
              default         no;
              10.0.0.0/8      yes;    # Internal
              203.0.0.0/8     yes;    # Example allowed range
          }
      
          server {
              location / {
                  if ($allowed_country = no) {
                      return 403;
                  }
                  proxy_pass http://backend;
              }
          }
      }
      ```
      
      ### GeoIP2 Module
      
      For geo-blocking or geo-routing by country. Requires `ngx_http_geoip2_module` and MaxMind GeoLite2 database.
      
      ```nginx
      # Load GeoIP2 module
      load_module modules/ngx_http_geoip2_module.so;
      
      http {
          geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
              auto_reload 60m;
              $geoip2_metadata_country_build metadata build_epoch;
              $geoip2_data_country_code country iso_code;
              $geoip2_data_country_name country names en;
          }
      
          # Block specific countries
          map $geoip2_data_country_code $blocked_country {
              default no;
              XX      yes;    # Replace XX with country code
              YY      yes;
          }
      
          server {
              if ($blocked_country = yes) {
                  return 403;
              }
          }
      }
      ```
      
      ### Combining IP and Authentication
      
      ```nginx
      location /admin/ {
          # Require BOTH IP match AND authentication
          satisfy all;
      
          allow 10.0.0.0/8;
          deny all;
      
          auth_basic "Admin Area";
          auth_basic_user_file /etc/nginx/.htpasswd;
      
          proxy_pass http://admin_backend;
      }
      
      location /internal/ {
          # Require EITHER IP match OR authentication
          satisfy any;
      
          allow 10.0.0.0/8;
          deny all;
      
          auth_basic "Internal Area";
          auth_basic_user_file /etc/nginx/.htpasswd;
      
          proxy_pass http://internal_backend;
      }
      ```
      
      ---
      
      ## Basic Authentication
      
      ### Setup
      
      ```bash
      # Install htpasswd utility
      sudo apt install apache2-utils    # Debian/Ubuntu
      sudo dnf install httpd-tools      # RHEL/Fedora
      
      # Create password file with first user
      sudo htpasswd -c /etc/nginx/.htpasswd admin
      
      # Add additional users (no -c flag!)
      sudo htpasswd /etc/nginx/.htpasswd user2
      
      # Use bcrypt hashing (more secure, requires htpasswd 2.4+)
      sudo htpasswd -B /etc/nginx/.htpasswd user3
      
      # Secure the file
      sudo chown root:www-data /etc/nginx/.htpasswd
      sudo chmod 640 /etc/nginx/.htpasswd
      ```
      
      ### Nginx Configuration
      
      ```nginx
      location /admin/ {
          auth_basic "Admin Area";
          auth_basic_user_file /etc/nginx/.htpasswd;
      
          proxy_pass http://admin_backend;
      }
      
      # Disable auth for specific sub-paths
      location /admin/health {
          auth_basic off;
          proxy_pass http://admin_backend;
      }
      ```
      
      ### Auth for Entire Site with Exceptions
      
      ```nginx
      server {
          listen 443 ssl http2;
          server_name staging.example.com;
      
          # Global auth for staging environment
          auth_basic "Staging Environment";
          auth_basic_user_file /etc/nginx/.htpasswd;
      
          location / {
              proxy_pass http://backend;
          }
      
          # Exempt health checks and webhooks
          location /health {
              auth_basic off;
              proxy_pass http://backend;
          }
      
          location /webhooks/ {
              auth_basic off;
              proxy_pass http://backend;
          }
      }
      ```
      
      ---
      
      ## Mutual TLS (mTLS)
      
      Mutual TLS requires both server and client to present certificates, providing strong authentication.
      
      ### Server Configuration
      
      ```nginx
      server {
          listen 443 ssl http2;
          server_name api.example.com;
      
          # Server certificate (standard)
          ssl_certificate     /etc/nginx/certs/server.pem;
          ssl_certificate_key /etc/nginx/certs/server.key;
      
          # CA certificate that signed client certificates
          ssl_client_certificate /etc/nginx/certs/client-ca.pem;
      
          # Require client certificate
          ssl_verify_client on;
          # Or make it optional:
          # ssl_verify_client optional;
      
          # Verification depth (how many intermediate CAs to check)
          ssl_verify_depth 2;
      
          # CRL for revoked client certificates
          ssl_crl /etc/nginx/certs/client-revoked.crl;
      
          location / {
              # Pass client certificate info to backend
              proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
              proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
              proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
              proxy_set_header X-SSL-Client-Fingerprint $ssl_client_fingerprint;
      
              proxy_pass http://backend;
          }
      }
      ```
      
      ### Optional Client Certificate
      
      ```nginx
      server {
          listen 443 ssl http2;
          server_name example.com;
      
          ssl_client_certificate /etc/nginx/certs/client-ca.pem;
          ssl_verify_client optional;
      
          location /public/ {
              # No client cert required
              proxy_pass http://backend;
          }
      
          location /secure/ {
              # Require valid client cert for this path
              if ($ssl_client_verify != SUCCESS) {
                  return 403;
              }
              proxy_pass http://secure_backend;
          }
      }
      ```
      
      ### Generate Client Certificates
      
      ```bash
      # 1. Create CA (one-time)
      openssl genrsa -out client-ca.key 4096
      openssl req -new -x509 -days 3650 -key client-ca.key -out client-ca.pem \
          -subj "/CN=Client CA"
      
      # 2. Generate client key and CSR
      openssl genrsa -out client.key 2048
      openssl req -new -key client.key -out client.csr \
          -subj "/CN=client-name/O=organization"
      
      # 3. Sign with CA
      openssl x509 -req -days 365 -in client.csr -CA client-ca.pem \
          -CAkey client-ca.key -CAcreateserial -out client.pem
      
      # 4. Create PKCS12 bundle for browser import
      openssl pkcs12 -export -out client.p12 \
          -inkey client.key -in client.pem -certfile client-ca.pem
      
      # 5. Test with curl
      curl --cert client.pem --key client.key https://api.example.com/
      ```
      
      ### Client Certificate Variables
      
      | Variable | Description |
      |----------|-------------|
      | `$ssl_client_verify` | `SUCCESS`, `FAILED:reason`, or `NONE` |
      | `$ssl_client_s_dn` | Subject DN of client certificate |
      | `$ssl_client_i_dn` | Issuer DN of client certificate |
      | `$ssl_client_serial` | Serial number of client certificate |
      | `$ssl_client_fingerprint` | SHA1 fingerprint of client certificate |
      | `$ssl_client_cert` | PEM-encoded client certificate |
      | `$ssl_client_raw_cert` | PEM-encoded client certificate (unescaped) |
      | `$ssl_client_escaped_cert` | URL-encoded client certificate |
      
      ---
      
      ## HTTP to HTTPS Redirect
      
      ### Standard Redirect
      
      ```nginx
      # Redirect all HTTP to HTTPS
      server {
          listen 80;
          server_name example.com www.example.com;
          return 301 https://example.com$request_uri;
      }
      ```
      
      ### Catch-All Redirect
      
      ```nginx
      # Redirect ANY domain on HTTP to HTTPS
      server {
          listen 80 default_server;
          server_name _;
          return 301 https://$host$request_uri;
      }
      ```
      
      ### Redirect with Let's Encrypt Exception
      
      ```nginx
      server {
          listen 80;
          server_name example.com www.example.com;
      
          # Allow ACME challenge for certificate renewal
          location /.well-known/acme-challenge/ {
              root /var/www/certbot;
          }
      
          # Redirect everything else to HTTPS
          location / {
              return 301 https://example.com$request_uri;
          }
      }
      ```
      
      ### WWW to Non-WWW (with HTTPS)
      
      ```nginx
      # Redirect www to non-www
      server {
          listen 443 ssl http2;
          server_name www.example.com;
      
          ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
      
          return 301 https://example.com$request_uri;
      }
      
      # Redirect HTTP www to HTTPS non-www
      server {
          listen 80;
          server_name www.example.com;
          return 301 https://example.com$request_uri;
      }
      ```
      
      ### Redirect with Preserved POST Body
      
      Note: `301` and `302` redirects convert POST to GET. Use `307`/`308` to preserve the method.
      
      ```nginx
      # 308 Permanent Redirect (preserves HTTP method)
      server {
          listen 80;
          server_name api.example.com;
          return 308 https://api.example.com$request_uri;
      }
      ```
      
      | Status | Permanent | Preserves Method |
      |--------|-----------|-----------------|
      | 301 | Yes | No (POST → GET) |
      | 302 | No | No (POST → GET) |
      | 307 | No | Yes |
      | 308 | Yes | Yes |
      
  • scripts
    • .gitkeep 0 B · in bundle
  • SKILL.md 14.8 KB
    ---
    name: nginx-ops
    description: "Nginx configuration, reverse proxy, SSL/TLS, load balancing, and performance tuning. Use for: nginx, reverse proxy, load balancer, proxy_pass, ssl certificate, lets encrypt, web server, location block, upstream, server block, nginx config, certbot, hsts, gzip, rate limiting."
    license: MIT
    allowed-tools: "Read Write Bash"
    metadata:
      author: claude-mods
      related-skills: docker-ops, security-ops, ci-cd-ops
    ---
    
    # Nginx Operations
    
    Comprehensive Nginx configuration, reverse proxy patterns, SSL/TLS hardening, load balancing strategies, and performance optimization for production deployments.
    
    ---
    
    ## Configuration Architecture Quick Reference
    
    ```
    nginx.conf (main context)
    ├── worker_processes auto;
    ├── worker_rlimit_nofile 65535;
    │
    ├── events {                          # Connection handling
    │   ├── worker_connections 4096;
    │   └── multi_accept on;
    │   }
    │
    ├── http {                            # HTTP server settings
    │   ├── include mime.types;
    │   ├── default_type application/octet-stream;
    │   ├── sendfile on;
    │   ├── gzip on;
    │   │
    │   ├── upstream backend {            # Load balancing pool
    │   │   └── server 127.0.0.1:3000;
    │   │   }
    │   │
    │   ├── server {                      # Virtual host
    │   │   ├── listen 443 ssl;
    │   │   ├── server_name example.com;
    │   │   │
    │   │   ├── location / {              # Request routing
    │   │   │   └── proxy_pass http://backend;
    │   │   │   }
    │   │   │
    │   │   └── location /static/ {
    │   │       └── root /var/www;
    │   │       }
    │   │   }
    │   │
    │   └── include /etc/nginx/conf.d/*.conf;
    │   }
    │
    └── stream {                          # TCP/UDP proxying (optional)
        └── server { ... }
        }
    ```
    
    ### Directive Inheritance Rules
    
    | Rule | Behavior | Example |
    |------|----------|---------|
    | **Inherit down** | Child blocks inherit parent directives | `gzip on;` in `http` applies to all `server` blocks |
    | **Override** | Child directive overrides parent | `gzip off;` in `location` overrides `http`-level `gzip on;` |
    | **Array directives** | NOT inherited - must be redeclared | `proxy_set_header` in `location` replaces ALL headers from `server` |
    | **No upward** | Inner blocks never affect outer | `location`-level settings don't affect `server` |
    
    **Critical:** Array-type directives (`proxy_set_header`, `add_header`, `proxy_hide_header`) are **completely replaced** when redefined in a child block, not merged. If you set one `proxy_set_header` in a `location`, you must redeclare ALL of them.
    
    ---
    
    ## Reverse Proxy Decision Tree
    
    ```
    Need to proxy requests?
    │
    ├─ Single backend server?
    │  └─ Use simple proxy_pass
    │     proxy_pass http://127.0.0.1:3000;
    │
    ├─ Multiple backend servers?
    │  │
    │  ├─ Need session persistence?
    │  │  ├─ By client IP → ip_hash
    │  │  └─ By cookie    → sticky cookie (Nginx Plus)
    │  │
    │  ├─ Backends have unequal capacity?
    │  │  └─ Use weight parameter
    │  │     server backend1:3000 weight=3;
    │  │     server backend2:3000 weight=1;
    │  │
    │  ├─ Want fewest active connections?
    │  │  └─ least_conn
    │  │
    │  ├─ Want even random distribution?
    │  │  └─ random two least_conn
    │  │
    │  └─ Default (no special needs)?
    │     └─ round-robin (default, no directive needed)
    │
    ├─ WebSocket connections?
    │  └─ Add Upgrade + Connection headers
    │     proxy_set_header Upgrade $http_upgrade;
    │     proxy_set_header Connection "upgrade";
    │
    ├─ gRPC backend?
    │  └─ Use grpc_pass grpc://backend;
    │
    └─ Streaming / Server-Sent Events?
       └─ Disable buffering
          proxy_buffering off;
    ```
    
    ---
    
    ## SSL/TLS Quick Start
    
    ### Let's Encrypt with Certbot
    
    ```bash
    # Install certbot
    sudo apt install certbot python3-certbot-nginx   # Debian/Ubuntu
    sudo dnf install certbot python3-certbot-nginx    # RHEL/Fedora
    
    # Obtain certificate (nginx plugin - easiest)
    sudo certbot --nginx -d example.com -d www.example.com
    
    # Obtain certificate (webroot - no nginx restart)
    sudo certbot certonly --webroot -w /var/www/html -d example.com
    
    # Test auto-renewal
    sudo certbot renew --dry-run
    ```
    
    ### Minimal Production SSL Config
    
    ```nginx
    server {
        listen 443 ssl http2;
        server_name example.com;
    
        # Certificates
        ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
        # Modern TLS (1.2 + 1.3)
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers off;
    
        # HSTS (2 years)
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    
        # OCSP Stapling
        ssl_stapling on;
        ssl_stapling_verify on;
        ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
        resolver 1.1.1.1 8.8.8.8 valid=300s;
        resolver_timeout 5s;
    
        # Session caching
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 1d;
        ssl_session_tickets off;
    
        root /var/www/example.com;
        index index.html;
    }
    
    # HTTP → HTTPS redirect
    server {
        listen 80;
        server_name example.com www.example.com;
        return 301 https://example.com$request_uri;
    }
    ```
    
    ---
    
    ## Location Matching Order
    
    Nginx evaluates `location` blocks in a specific priority order, **not** in the order they appear in the config file.
    
    | Priority | Modifier | Type | Example | Behavior |
    |----------|----------|------|---------|----------|
    | 1 | `=` | Exact match | `location = /favicon.ico` | Stops search immediately on match |
    | 2 | `^~` | Prefix (no regex) | `location ^~ /static/` | Stops search if this prefix matches (skips regex) |
    | 3 | `~` | Regex (case-sensitive) | `location ~ \.php$` | First matching regex wins |
    | 3 | `~*` | Regex (case-insensitive) | `location ~* \.(jpg\|png)$` | First matching regex wins |
    | 4 | _(none)_ | Prefix | `location /api/` | Longest prefix wins (but only after regex check) |
    
    ### Evaluation Algorithm
    
    1. Check all **prefix** locations, remember the **longest** match
    2. If longest match has `^~` modifier → use it, stop
    3. Check **regex** locations in config-file order → first match wins
    4. If no regex matches → use the longest prefix from step 1
    5. `= /path` is checked first and wins immediately if matched
    
    ### Example
    
    ```nginx
    location = /             { }  # Only exact "/"
    location /               { }  # Catch-all prefix
    location /api/           { }  # Prefix: /api/*
    location ^~ /static/     { }  # Prefix, skip regex: /static/*
    location ~ \.php$        { }  # Regex: any .php file
    location ~* \.(gif|jpg)$ { }  # Case-insensitive regex: images
    ```
    
    | Request URI | Matched Location | Why |
    |-------------|-----------------|-----|
    | `/` | `= /` | Exact match (priority 1) |
    | `/index.html` | `/` | Longest prefix, no regex match |
    | `/api/users` | `/api/` | Longest prefix, no regex match |
    | `/static/logo.png` | `^~ /static/` | `^~` skips regex check |
    | `/app/index.php` | `~ \.php$` | Regex beats prefix |
    | `/photos/cat.jpg` | `~* \.(gif\|jpg)$` | Regex beats prefix |
    
    ---
    
    ## Common Configurations
    
    ### SPA Routing (React, Vue, Angular)
    
    ```nginx
    server {
        listen 80;
        server_name app.example.com;
    
        root /var/www/app/dist;
        index index.html;
    
        # Serve static files directly, fall back to index.html for SPA routes
        location / {
            try_files $uri $uri/ /index.html;
        }
    
        # Cache static assets aggressively
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }
    ```
    
    ### WebSocket Proxy
    
    ```nginx
    location /ws/ {
        proxy_pass http://127.0.0.1:3000;
        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_read_timeout 86400s;  # Keep WebSocket alive for 24h
        proxy_send_timeout 86400s;
    }
    ```
    
    ### Rate Limiting
    
    ```nginx
    # Define zone: 10MB shared memory, 10 requests/second per IP
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    
    server {
        location /api/ {
            # Allow burst of 20, process excess without delay up to burst
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
    
            proxy_pass http://backend;
        }
    }
    ```
    
    ### Gzip Compression
    
    ```nginx
    http {
        gzip on;
        gzip_comp_level 5;          # Balance CPU vs compression (1-9)
        gzip_min_length 256;        # Don't compress tiny responses
        gzip_vary on;               # Vary: Accept-Encoding header
        gzip_proxied any;           # Compress proxied responses too
        gzip_types
            text/plain
            text/css
            text/javascript
            application/javascript
            application/json
            application/xml
            application/xml+rss
            image/svg+xml;
    }
    ```
    
    ### Static File Serving
    
    ```nginx
    location /static/ {
        alias /var/www/static/;     # Note: alias, not root (includes /static/ path)
        expires 30d;
        add_header Cache-Control "public, no-transform";
    
        # Disable access log for static files
        access_log off;
    
        # Enable open file cache
        open_file_cache max=1000 inactive=20s;
        open_file_cache_valid 30s;
        open_file_cache_min_uses 2;
    }
    ```
    
    ### CORS Headers
    
    ```nginx
    location /api/ {
        # CORS headers
        add_header Access-Control-Allow-Origin "https://app.example.com" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
        add_header Access-Control-Max-Age 86400 always;
    
        # Handle preflight requests
        if ($request_method = OPTIONS) {
            return 204;
        }
    
        proxy_pass http://backend;
    }
    ```
    
    ---
    
    ## Docker Patterns
    
    ### Nginx as Reverse Proxy in Docker Compose
    
    ```yaml
    # docker-compose.yml
    services:
      nginx:
        image: nginx:alpine
        ports:
          - "80:80"
          - "443:443"
        volumes:
          - ./nginx.conf:/etc/nginx/nginx.conf:ro
          - ./certs:/etc/nginx/certs:ro
        depends_on:
          - app
        networks:
          - webnet
    
      app:
        build: .
        expose:
          - "3000"    # Internal only, not published to host
        networks:
          - webnet
    
    networks:
      webnet:
    ```
    
    ```nginx
    # nginx.conf for docker-compose (use service name as hostname)
    upstream app_backend {
        server app:3000;    # Docker DNS resolves service name
    }
    
    server {
        listen 80;
        location / {
            proxy_pass http://app_backend;
            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;
        }
    }
    ```
    
    ### Multi-Stage Build with Static Assets
    
    ```dockerfile
    # Stage 1: Build frontend
    FROM node:20-alpine AS builder
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci
    COPY . .
    RUN npm run build
    
    # Stage 2: Serve with nginx
    FROM nginx:alpine
    COPY --from=builder /app/dist /usr/share/nginx/html
    COPY nginx.conf /etc/nginx/conf.d/default.conf
    EXPOSE 80
    ```
    
    ```nginx
    # nginx.conf for containerized SPA
    server {
        listen 80;
        root /usr/share/nginx/html;
        index index.html;
    
        # SPA routing
        location / {
            try_files $uri $uri/ /index.html;
        }
    
        # Health check endpoint
        location /health {
            access_log off;
            return 200 "OK\n";
            add_header Content-Type text/plain;
        }
    
        # Cache busted assets
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }
    ```
    
    ---
    
    ## Common Gotchas
    
    | Gotcha | Why | Fix |
    |--------|-----|-----|
    | **Trailing slash in `proxy_pass`** | `proxy_pass http://backend` keeps `/api/users` as-is; `proxy_pass http://backend/` strips the matched `location` prefix | Be intentional: with `/` to strip prefix, without to preserve |
    | **Missing proxy headers** | Backend sees nginx's IP, not the client's. Breaks auth, logging, and geo detection | Always set `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, and `Host` |
    | **Buffer size errors (502)** | Large headers (cookies, JWTs) exceed default buffer sizes | Increase `proxy_buffer_size 8k;` and `proxy_buffers 4 16k;` |
    | **`worker_connections` too low** | Default is 512 or 1024; each client uses 2 connections (client + upstream) | Set `worker_connections 4096;` and raise `worker_rlimit_nofile` |
    | **`try_files` with `proxy_pass`** | `try_files` and `proxy_pass` in the same `location` don't work as expected | Use `try_files $uri @backend;` with a named location for proxy |
    | **"if is evil"** | `if` inside `location` creates an implicit nested location, breaking directives | Use `map` for variable-based logic; reserve `if` for `return`/`rewrite` only |
    | **Resolver for dynamic upstreams** | Variables in `proxy_pass` (e.g., `$upstream`) bypass startup DNS resolution | Add `resolver 127.0.0.11 valid=30s;` (Docker) or `resolver 1.1.1.1;` |
    | **Missing `index` directive** | Returns 403 Forbidden when accessing a directory instead of index file | Add `index index.html;` in `server` or `location` block |
    | **Permission denied on socket** | Nginx worker can't read the upstream Unix socket | Ensure nginx user is in the socket's group; `chmod 660` the socket |
    | **Duplicate `Content-Encoding` with gzip** | Upstream already compresses + nginx gzip double-compresses | Use `gzip_proxied` carefully or `proxy_set_header Accept-Encoding "";` |
    | **`add_header` not inherited** | Adding ANY `add_header` in a `location` discards ALL parent `add_header` directives | Redeclare all headers in the child block, or use `include` for shared headers |
    | **`alias` vs `root` confusion** | `root` appends the location path; `alias` replaces it. `/img/` + `root /data` = `/data/img/`; `alias /data/` = `/data/` | Use `alias` when location path shouldn't appear in filesystem path |
    
    ---
    
    ## Reference Files
    
    | File | Contents | Lines |
    |------|----------|-------|
    | [reverse-proxy.md](references/reverse-proxy.md) | Upstream blocks, load balancing, proxy caching, WebSocket/gRPC, timeouts, real-world configs | ~650 |
    | [ssl-security.md](references/ssl-security.md) | TLS config, Let's Encrypt, HSTS, OCSP, security headers, rate limiting, mTLS | ~550 |
    | [performance.md](references/performance.md) | Worker tuning, compression, caching, HTTP/2+3, static files, monitoring | ~550 |
    
    ---
    
    ## See Also
    
    - **docker-ops** - Container orchestration, docker-compose patterns
    - **security-ops** - Application security, authentication patterns
    - **ci-cd-ops** - Deployment pipelines, zero-downtime deploys
    - [Nginx official docs](https://nginx.org/en/docs/)
    - [Mozilla SSL Configuration Generator](https://ssl-config.mozilla.org/)
    - [Nginx Config Generator](https://www.digitalocean.com/community/tools/nginx)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related