Cybersecurity-Projects/PROJECTS/bug-bounty-platform/docs/research/NGINX.md

27 KiB

Production-Grade Nginx Configuration: 2025 Best Practices

Complete guide for modern full-stack applications (FastAPI + Frontend) with emphasis on WebSocket support, configuration organization, and dev/prod architectures.

Table of Contents

  1. Configuration File Organization
  2. WebSocket Proxying
  3. Shared Configuration (http.conf)
  4. Development Configuration
  5. Production Configuration
  6. Performance Optimization
  7. Security Headers
  8. Static File Serving
  9. Vite Dev Server Integration
  10. Load Balancing & Upstreams
  11. Rate Limiting
  12. Logging & Monitoring
  13. Complete Configuration Examples

Configuration File Organization

conf/nginx/
├── http.conf           # Shared: upstreams, maps, global http settings
├── dev.nginx          # Full nginx.conf for development
└── prod.nginx         # Full nginx.conf for production

What Goes Where

http.conf (Shared Configurations):

  • Upstream definitions (backend, frontend servers)
  • Map directives for WebSocket connection upgrades
  • Shared rate limit zones
  • Common proxy settings
  • MIME type definitions
  • Log formats

dev.nginx (Development-Specific):

  • Full nginx.conf structure
  • Includes http.conf
  • Proxies to Vite dev server (port 5173)
  • Proxies API/WebSocket to backend (port 8000)
  • Verbose logging
  • CORS permissive settings
  • No caching
  • No SSL (typically)

prod.nginx (Production-Specific):

  • Full nginx.conf structure
  • Includes http.conf
  • Serves static files from /usr/share/nginx/html
  • Proxies API/WebSocket to Gunicorn workers
  • SSL/TLS configuration
  • Security headers
  • Gzip/Brotli compression
  • Static file caching
  • Error logging only

WebSocket Proxying

WebSocket connections require explicit handling because the "Upgrade" and "Connection" headers are hop-by-hop and not automatically passed to the proxied server. The modern approach uses a map directive to handle connections conditionally.

Core WebSocket Configuration Pattern

# In http.conf (shared configuration)
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

This sophisticated approach sets the Connection header to "close" when there's no Upgrade header, and to "upgrade" when WebSocket upgrade is requested.

WebSocket Location Block

location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    
    # WebSocket-specific 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;
    proxy_set_header X-Forwarded-Proto $scheme;
    
    # Timeout settings for long-lived connections
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_connect_timeout 75s;
    
    # Disable buffering for real-time data
    proxy_buffering off;
    proxy_cache_bypass $http_upgrade;
}

Critical WebSocket Settings Explained

For WebSocket session persistence with multiple backend servers, use ip_hash to ensure clients always connect to the same backend:

upstream websocket_backend {
    ip_hash;  # Session persistence
    server backend1:8000;
    server backend2:8000;
    server backend3:8000;
}

Timeout Configuration: By default, connections close if the proxied server doesn't transmit data within 60 seconds. For WebSockets:

  • proxy_read_timeout: Set to 3600s (1 hour) or higher
  • proxy_send_timeout: Set to 3600s (1 hour) or higher
  • proxy_connect_timeout: Usually 75s is sufficient

Shared Configuration (http.conf)

This file contains settings used by both dev and prod environments.

# conf/nginx/http.conf

# WebSocket upgrade handling
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

# Upstream definitions
upstream backend {
    # Development: single uvicorn instance
    # Production: multiple gunicorn workers (override in prod.nginx)
    server backend:8000 max_fails=3 fail_timeout=30s;
    keepalive 32;  # Connection pooling
}

upstream frontend_dev {
    # Only used in development
    server frontend:5173;
}

# Custom log format with timing information
log_format main_timed '$remote_addr - $remote_user [$time_local] '
                      '"$request" $status $body_bytes_sent '
                      '"$http_referer" "$http_user_agent" '
                      'rt=$request_time uct="$upstream_connect_time" '
                      'uht="$upstream_header_time" urt="$upstream_response_time"';

# Rate limit zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=1r/s;

# Connection limits
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

# Common proxy settings
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;

Development Configuration (dev.nginx)

Full nginx.conf optimized for local development with hot module replacement.

# conf/nginx/dev.nginx

user nginx;
worker_processes 1;  # Single worker sufficient for dev
error_log /var/log/nginx/error.log debug;  # Verbose logging
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # Include shared configuration
    include /etc/nginx/http.conf;
    
    # Development-specific settings
    access_log /var/log/nginx/access.log main_timed;
    sendfile off;  # Disable for file system changes
    tcp_nopush off;
    tcp_nodelay on;
    keepalive_timeout 65;
    
    # Disable caching in development
    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
    
    # CORS permissive for development
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
    
    server {
        listen 80;
        server_name localhost;
        
        # Handle preflight requests
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }
        
        # API routes to backend
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            limit_conn conn_limit 10;
            
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            
            # Proxy buffering settings
            proxy_buffering off;
            proxy_request_buffering off;
        }
        
        # WebSocket route
        location /ws/ {
            proxy_pass http://backend;
            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;
            proxy_buffering off;
        }
        
        # Vite dev server (with HMR WebSocket support)
        location / {
            proxy_pass http://frontend_dev;
            proxy_http_version 1.1;
            
            # Required for Vite HMR
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
            proxy_set_header Host $host;
            
            # Vite-specific timeout
            proxy_read_timeout 60s;
            proxy_buffering off;
        }
        
        # Health check endpoint
        location /health {
            access_log off;
            return 200 "healthy\n";
            add_header Content-Type text/plain;
        }
    }
}

Key Development Features:

  1. Verbose Logging: error_log debug for troubleshooting
  2. No Caching: Ensures fresh content on every request
  3. Permissive CORS: Allows frontend to call backend freely
  4. HMR Support: WebSocket headers (Upgrade and Connection) are required for Vite's Hot Module Replacement to function properly
  5. Disabled Optimizations: sendfile off to catch file changes immediately

Production Configuration (prod.nginx)

Optimized for performance, security, and reliability.

# conf/nginx/prod.nginx

user nginx;
worker_processes auto;  # One per CPU core
worker_rlimit_nofile 100000;
error_log /var/log/nginx/error.log warn;  # Only warnings and errors
pid /var/run/nginx.pid;

events {
    worker_connections 4096;  # High concurrency support
    use epoll;  # Efficient connection handling on Linux
    multi_accept on;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # Include shared configuration
    include /etc/nginx/http.conf;
    
    # Logging with buffering
    access_log /var/log/nginx/access.log main_timed buffer=32k flush=5s;
    
    # Performance optimizations
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    keepalive_requests 100;
    types_hash_max_size 2048;
    server_tokens off;  # Hide nginx version
    
    # File cache
    open_file_cache max=10000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;
    
    # Buffer sizes
    client_body_buffer_size 128k;
    client_header_buffer_size 16k;
    client_max_body_size 10m;
    large_client_header_buffers 4 16k;
    
    # Timeouts
    client_body_timeout 12s;
    client_header_timeout 12s;
    send_timeout 10s;
    
    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;
    gzip_disable "msie6";
    gzip_min_length 256;
    
    # Redirect HTTP to HTTPS
    server {
        listen 80;
        server_name yourdomain.com;
        return 301 https://$host$request_uri;
    }
    
    # Main HTTPS server
    server {
        listen 443 ssl http2;
        server_name yourdomain.com;
        
        # SSL configuration
        ssl_certificate /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;
        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;
        ssl_session_cache shared:SSL:50m;
        ssl_session_timeout 1d;
        ssl_session_tickets off;
        ssl_stapling on;
        ssl_stapling_verify on;
        
        # Security headers
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
        add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
        
        # Content Security Policy
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss://yourdomain.com" always;
        
        # Root directory for static files
        root /usr/share/nginx/html;
        index index.html;
        
        # API routes with rate limiting
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            limit_conn conn_limit 20;
            
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            
            # Proxy buffering optimized for API
            proxy_buffering on;
            proxy_buffers 8 24k;
            proxy_buffer_size 2k;
            
            # Timeouts
            proxy_connect_timeout 75s;
            proxy_send_timeout 30s;
            proxy_read_timeout 30s;
        }
        
        # Auth endpoints with stricter rate limiting
        location /api/auth/ {
            limit_req zone=auth_limit burst=5 nodelay;
            
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }
        
        # WebSocket route
        location /ws/ {
            proxy_pass http://backend;
            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;
            proxy_connect_timeout 75s;
            proxy_buffering off;
        }
        
        # Static files with aggressive caching
        location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
            access_log off;
        }
        
        location ~* \.(css|js)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
            access_log off;
        }
        
        location ~* \.(woff|woff2|ttf|eot|otf)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
            access_log off;
        }
        
        # SPA fallback - all non-matching routes to index.html
        location / {
            try_files $uri $uri/ /index.html;
            add_header Cache-Control "no-cache, must-revalidate";
        }
        
        # Health check
        location /health {
            access_log off;
            return 200 "healthy\n";
            add_header Content-Type text/plain;
        }
        
        # Deny access to hidden files
        location ~ /\. {
            deny all;
            access_log off;
            log_not_found off;
        }
    }
}

Performance Optimization

Worker Configuration

Running one worker process per CPU core works well in most cases, and setting worker_processes to auto achieves this:

worker_processes auto;
worker_rlimit_nofile 100000;  # File descriptor limit

events {
    worker_connections 4096;
    use epoll;  # Most efficient on Linux
    multi_accept on;  # Accept multiple connections at once
}

Connection and Request Handling

The default worker_connections is 512, but most systems can support higher values. The optimal setting depends on server resources and traffic patterns.

Formula: max_clients = worker_processes * worker_connections

Buffer Optimization

If buffer sizes are too low, Nginx will write to temporary files, causing excessive disk I/O:

client_body_buffer_size 128k;
client_header_buffer_size 16k;
client_max_body_size 10m;
large_client_header_buffers 4 16k;

Keepalive Connections

Keepalive connections reduce CPU and network overhead by keeping connections open longer:

keepalive_timeout 65;
keepalive_requests 100;

upstream backend {
    server backend:8000;
    keepalive 32;  # Idle connections to upstream
}

Sendfile and TCP Optimizations

The sendfile() system call enables zero-copy data transfer, speeding up TCP transmissions without consuming CPU cycles:

sendfile on;
tcp_nopush on;  # Send headers in one packet
tcp_nodelay on;  # Disable Nagle's algorithm

File Caching

open_file_cache max=10000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;

Compression

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;  # Balance between CPU and compression
gzip_min_length 256;
gzip_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/json
    application/javascript
    application/xml+rss
    application/atom+xml
    image/svg+xml;

Important: Don't increase compression level too high, as it costs CPU effort without proportional throughput gains.


Security Headers

Essential Security Headers (2025)

Security headers are levers that slash XSS risk, lock browsers to HTTPS, tame third-party scripts, and protect users without touching app code.

# HTTP Strict Transport Security (HSTS)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;

# Prevent MIME sniffing
add_header X-Content-Type-Options "nosniff" always;

# XSS Protection (legacy but still useful)
add_header X-XSS-Protection "1; mode=block" always;

# Referrer Policy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Disable dangerous browser features
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

Content Security Policy (CSP)

CSP controls where scripts, styles, images, frames, and connections can load from. Start with a restrictive policy:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss://yourdomain.com" always;

Best Practice: Roll out CSP in Report-Only mode first, sending violation reports to an endpoint you control:

# Test mode
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;

HSTS Explained

HSTS forces browsers to only use HTTPS by caching this policy for the max-age period. The includeSubDomains directive applies the policy to all subdomains.

Preloading: To add your domain to the browser preload list, include "preload" in the header and submit to hstspreload.org. This is a one-way decision—removal is difficult.

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

SSL/TLS Best Practices

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;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;

Static File Serving (Production)

Cache Headers Strategy

Use Cache-Control on static files so that CDNs and browsers can cache them effectively. The modern approach favors Cache-Control over Expires.

# Images, fonts, media - long cache
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|avif|woff|woff2|ttf|eot|otf|mp4|mp3|ogg|webm)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}

# CSS and JavaScript - long cache with versioning
location ~* \.(css|js)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}

# HTML files - no caching
location ~* \.html$ {
    expires -1;
    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}

Cache Busting

When files are updated, use cache-busting by appending version numbers to filenames (e.g., style.css?v=2 or style.v2.css).

SPA Routing

For Single Page Applications, route all non-file requests to index.html:

location / {
    try_files $uri $uri/ /index.html;
    add_header Cache-Control "no-cache, must-revalidate";
}

Vite Dev Server Integration (Development)

HMR WebSocket Support

Vite's HMR requires WebSocket support with Upgrade and Connection headers set for the WebSocket connection to function:

location / {
    proxy_pass http://frontend:5173;
    proxy_http_version 1.1;
    
    # Critical for Vite HMR
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host $host;
    
    # Timeouts
    proxy_read_timeout 60s;
    proxy_buffering off;
}

Vite HMR Path Handling

If Vite uses a custom HMR path:

location ~* /__vite_hmr {
    proxy_pass http://frontend:5173;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
}

Common Vite + Nginx Issues

  1. HMR not working: Ensure WebSocket headers are set correctly
  2. Too many redirects: Use proxy_pass http://host; without trailing slash to avoid path manipulation
  3. Connection timeout: Default proxy timeouts may be too short; increase to 30-60 seconds for WebSocket connections

Load Balancing & Upstreams

Upstream Configuration

upstream backend {
    least_conn;  # Route to server with fewest connections
    
    server backend1:8000 max_fails=3 fail_timeout=30s;
    server backend2:8000 max_fails=3 fail_timeout=30s;
    server backend3:8000 backup;  # Fallback server
    
    keepalive 32;  # Connection pooling
    keepalive_requests 100;
    keepalive_timeout 60s;
}

Load Balancing Methods

  • round_robin (default): Distribute requests evenly
  • least_conn: Route to server with fewest active connections
  • ip_hash: Consistent routing based on client IP (session persistence)
  • hash $request_uri consistent: Route based on URI

Health Checks (Nginx Plus)

upstream backend {
    zone backend 64k;
    
    server backend1:8000;
    server backend2:8000;
    
    health_check interval=5s fails=3 passes=2;
}

Rate Limiting

Zone Definitions

# In http block
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

Application in Locations

# General API rate limiting
location /api/ {
    limit_req zone=api_limit burst=20 nodelay;
    limit_conn conn_limit 20;
    proxy_pass http://backend;
}

# Stricter for authentication
location /api/auth/ {
    limit_req zone=auth_limit burst=5 nodelay;
    proxy_pass http://backend;
}

Parameters:

  • rate: Requests per second (or r/m for per minute)
  • burst: Allow temporary burst above rate
  • nodelay: Process burst requests immediately
  • limit_conn: Max simultaneous connections

Logging & Monitoring

Custom Log Format with Timing

log_format main_timed '$remote_addr - $remote_user [$time_local] '
                      '"$request" $status $body_bytes_sent '
                      '"$http_referer" "$http_user_agent" '
                      'rt=$request_time uct="$upstream_connect_time" '
                      'uht="$upstream_header_time" urt="$upstream_response_time"';

Buffered Logging for Performance

Logging every request directly to disk is expensive; buffering reduces write operations:

access_log /var/log/nginx/access.log main_timed buffer=32k flush=5s;

Conditional Logging

# Don't log health checks
location /health {
    access_log off;
    return 200 "healthy\n";
}

# Log only errors for static files
location ~* \.(jpg|png|css|js)$ {
    access_log off;
    error_log /var/log/nginx/static_error.log;
}

Complete Configuration Examples

Docker Compose Integration

# docker-compose.prod.yml
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./conf/nginx/http.conf:/etc/nginx/http.conf:ro
      - ./conf/nginx/prod.nginx:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
      - nginx_cache:/var/cache/nginx
    depends_on:
      - backend
    networks:
      - app_network

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.prod
    expose:
      - "8000"
    networks:
      - app_network

volumes:
  nginx_cache:

networks:
  app_network:

Minimal Dev Configuration

# Absolute minimum for development
http {
    include mime.types;
    
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }
    
    upstream backend {
        server backend:8000;
    }
    
    server {
        listen 80;
        
        location /api/ {
            proxy_pass http://backend;
        }
        
        location /ws/ {
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
            proxy_read_timeout 3600s;
        }
        
        location / {
            proxy_pass http://frontend:5173;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
        }
    }
}

Key Takeaways

WebSocket Requirements

  1. Map $http_upgrade to $connection_upgrade
  2. Set proxy_http_version 1.1
  3. Set headers: Upgrade $http_upgrade and Connection $connection_upgrade
  4. Increase timeouts: proxy_read_timeout 3600s
  5. Disable buffering: proxy_buffering off

Dev vs Prod Differences

  • Dev: Verbose logging, no caching, permissive CORS, proxy to Vite
  • Prod: Error logging only, aggressive caching, strict security headers, serve static files

File Organization

  • http.conf: Upstreams, maps, shared settings
  • dev.nginx: Full config optimized for development
  • prod.nginx: Full config optimized for production

Performance Priorities

  1. Worker processes = CPU cores (worker_processes auto)
  2. High worker connections (4096+)
  3. Enable sendfile, tcp_nopush, tcp_nodelay
  4. Buffer optimization to avoid disk I/O
  5. Keepalive connections for upstreams

Security Essentials

  1. HSTS with preload for HTTPS enforcement
  2. Comprehensive CSP to prevent XSS
  3. X-Frame-Options to prevent clickjacking
  4. Rate limiting on API endpoints
  5. Hide server version (server_tokens off)

Static File Serving

  1. Long cache for assets (1 year with immutable)
  2. No cache for HTML files
  3. Use cache busting with versioned filenames
  4. SPA fallback with try_files $uri /index.html