Skip to main content
← All Articles Β· Β· 11 min read

Nginx Configuration Guide for Web Developers 2026: Reverse Proxy, SSL & Performance

Master Nginx configuration in 2026. Complete guide covering reverse proxy setup, SSL/TLS with Let's Encrypt, gzip compression, caching, rate limiting, load balancing, and common gotchas.

nginxweb-serverdevopssslperformancereverse-proxy

Nginx powers roughly 34% of all web servers on the internet. It’s the workhorse behind reverse proxies, load balancers, static file servers, and API gateways for applications of every scale. Despite its ubiquity, its configuration syntax trips up developers constantly β€” a misplaced semicolon or a wrong location block order can silently break an entire site.

This guide walks through every configuration pattern you’ll actually need as a web developer in 2026, with complete, production-tested config blocks for each.


Nginx Block Structure

Before diving into specific features, understanding Nginx’s block hierarchy is essential:

# /etc/nginx/nginx.conf β€” top-level structure

# Global context (worker processes, user, pid)
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    # Connection handling
    worker_connections 1024;
    use epoll;              # Linux only β€” fastest event model
    multi_accept on;
}

http {
    # HTTP-level settings (MIME types, logging, gzip, etc.)
    include       mime.types;
    default_type  application/octet-stream;

    # Include all virtual hosts
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Each site lives in a separate server {} block:

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/example.com;

    location / {
        # Request handling
    }

    location /api/ {
        # API-specific handling
    }
}

Block precedence rules (critical to understand):

  1. server {} β€” matches by IP/port/hostname
  2. location {} β€” matches by URL path (exact = > prefix ^~ > regex ~ > prefix /)

Reverse Proxy Setup

The most common Nginx use case is proxying requests to an upstream application server (Node.js, Python, Go, etc.):

# /etc/nginx/conf.d/myapp.conf

upstream app_backend {
    server 127.0.0.1:3000;
    keepalive 32;           # Persistent connections to upstream
}

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

    # Redirect HTTP to HTTPS
    return 301 https://$host$request_uri;
}

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

    # SSL config (see next section)
    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    # Proxy to upstream
    location / {
        proxy_pass http://app_backend;

        # Essential proxy headers
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';    # WebSocket support
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Timeouts
        proxy_connect_timeout 30s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # Buffer settings
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
    }
}

Common gotcha: Without proxy_set_header Host $host, your app receives localhost as the host instead of the real domain name, which breaks URL generation in frameworks like Rails, Django, and Next.js.


SSL/TLS with Let’s Encrypt

Let’s Encrypt provides free, auto-renewing SSL certificates. Here’s the complete setup with security best practices:

Step 1 β€” Install Certbot and obtain a certificate:

# Ubuntu/Debian
apt install certbot python3-certbot-nginx

# Obtain certificate
certbot --nginx -d example.com -d www.example.com

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

Step 2 β€” Nginx SSL configuration with strong security settings:

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

    # Certificate paths (managed by Certbot)
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Modern TLS β€” disable old, vulnerable protocols
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    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;

    # Session resumption (performance)
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP Stapling β€” faster certificate validation
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    resolver 8.8.8.8 8.8.4.4 valid=300s;

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

HSTS note: The Strict-Transport-Security header tells browsers to always use HTTPS. The preload directive enables HSTS preloading β€” only add it when you’re 100% committed to HTTPS on your domain and all subdomains.


Gzip Compression

Gzip reduces transfer sizes by 60–80% for text-based assets. Add these settings to the http {} block in nginx.conf:

http {
    # Enable gzip
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;          # 1-9, sweet spot is 5-6
    gzip_min_length 1024;       # Don't compress tiny files
    gzip_buffers 16 8k;
    gzip_http_version 1.1;

    # Types to compress
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml
        application/xml+rss
        application/atom+xml
        application/ld+json
        image/svg+xml
        font/truetype
        font/opentype
        application/vnd.ms-fontobject;
}

What not to compress: Images (JPEG, PNG, WebP), videos, and audio files are already compressed β€” gzipping them wastes CPU with negligible size benefit. The gzip_types list intentionally excludes these.


Caching Headers

Static assets should be aggressively cached. Dynamic API responses should not be cached by default:

server {
    # Static assets β€” cache for 1 year (cache-busting via filename hashing)
    location ~* \.(js|css|woff2|woff|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Images β€” cache for 30 days
    location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|avif)$ {
        expires 30d;
        add_header Cache-Control "public, no-transform";
        access_log off;
    }

    # HTML β€” short cache or no cache (let browser revalidate)
    location ~* \.html$ {
        expires -1;
        add_header Cache-Control "no-cache, must-revalidate";
    }

    # API routes β€” no caching
    location /api/ {
        proxy_pass http://app_backend;
        add_header Cache-Control "no-store";
    }
}

Nginx proxy cache (server-side caching):

http {
    # Define cache zone (1GB, metadata stored in /tmp/nginx_cache)
    proxy_cache_path /tmp/nginx_cache
        levels=1:2
        keys_zone=CACHE:10m
        max_size=1g
        inactive=60m
        use_temp_path=off;
}

server {
    location / {
        proxy_pass http://app_backend;
        proxy_cache CACHE;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating;
        proxy_cache_lock on;

        # Show cache hit/miss in response headers (useful for debugging)
        add_header X-Cache-Status $upstream_cache_status;
    }
}

Rate Limiting

Rate limiting protects your application from abuse, scrapers, and DDoS:

http {
    # Define rate limit zones
    # $binary_remote_addr β€” per IP address
    # 10m β€” 10MB memory for the zone (stores ~160k IPs)
    # rate=10r/s β€” 10 requests per second

    limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

    limit_req_status 429;       # Return 429 Too Many Requests (not 503)
    limit_conn_status 429;
}

server {
    # Apply general rate limiting to all routes
    limit_req zone=general burst=20 nodelay;

    # Stricter limit on API
    location /api/ {
        limit_req zone=api burst=50 nodelay;
        proxy_pass http://app_backend;
    }

    # Very strict on auth endpoints (prevent brute force)
    location /api/auth/login {
        limit_req zone=login burst=3;
        proxy_pass http://app_backend;
    }
}

burst explained: If the rate is 10r/s with burst=20, Nginx queues up to 20 excess requests and serves them. nodelay processes the burst immediately without artificial delay β€” useful for APIs, bad for login endpoints.


Load Balancing

When you have multiple application instances, Nginx distributes traffic across them:

upstream app_cluster {
    # Default: round-robin
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
    server 10.0.0.3:3000;

    # Weighted round-robin (server 1 gets 3x traffic)
    # server 10.0.0.1:3000 weight=3;
    # server 10.0.0.2:3000 weight=1;

    # Least connections β€” best for mixed workloads
    # least_conn;

    # IP hash β€” sticky sessions (same client always β†’ same server)
    # ip_hash;

    # Health check (Nginx Plus only; OSS uses passive checks)
    # check interval=3000 rise=2 fall=5 timeout=1000;

    keepalive 32;
}

server {
    location / {
        proxy_pass http://app_cluster;
        proxy_next_upstream error timeout http_500 http_502 http_503;
        proxy_next_upstream_tries 3;
    }
}

When to use which algorithm:

  • Round-robin (default): Equal servers, stateless apps
  • Least connections: Mixed request durations (API + file uploads)
  • IP hash: Apps with server-side sessions (legacy apps)
  • Weighted: Servers with different capacities

Common Gotchas

1. location block order matters

server {
    # WRONG β€” this catches /api/health too, so it never reaches the specific rule below
    location /api/ {
        proxy_pass http://app_backend;
    }

    location = /api/health {
        return 200 "OK";
    }
}

server {
    # CORRECT β€” exact match takes priority over prefix match
    location = /api/health {
        return 200 "OK";
    }

    location /api/ {
        proxy_pass http://app_backend;
    }
}

2. Trailing slash in proxy_pass

# These behave differently:

# Without trailing slash β€” passes /api/users as-is to upstream
proxy_pass http://backend;

# With trailing slash β€” strips /api prefix, passes /users to upstream
location /api/ {
    proxy_pass http://backend/;
}

3. 413 Request Entity Too Large

Default max body size is 1MB. Increase it for file upload endpoints:

# In http {}, server {}, or location {} block
client_max_body_size 50M;

4. WebSocket connections drop

Without the Upgrade and Connection headers, WebSocket connections fail silently:

location /ws/ {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_read_timeout 86400;   # Keep WS alive for up to 24h
}

5. Test config before reloading

Never reload Nginx without testing the config first:

nginx -t                        # Test configuration
nginx -t && nginx -s reload     # Safe reload (atomic, zero downtime)

Complete Production Config Template

Here’s a battle-tested template combining all the above patterns:

# /etc/nginx/conf.d/production.conf

upstream app {
    server 127.0.0.1:3000;
    keepalive 32;
}

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

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header X-Content-Type-Options nosniff;

    gzip on;
    gzip_types text/css application/javascript application/json;

    client_max_body_size 25M;

    location ~* \.(js|css|woff2|png|jpg|webp|svg)$ {
        root /var/www/example.com;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    location / {
        limit_req zone=ratelimit burst=30 nodelay;
        proxy_pass http://app;
        proxy_http_version 1.1;
        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 Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 60s;
    }
}

Generate and Validate Configs Automatically

Writing Nginx config by hand is error-prone. Use DevPlaybook’s tooling to speed up the process:

  • Nginx Config Generator β€” generate production-ready Nginx configs from a form interface. Select your use case (reverse proxy, static site, load balancer), fill in your domains and upstream ports, and get a complete config file ready to deploy.

The generator handles the common gotchas automatically and outputs configs that pass nginx -t on first try.


Summary

PatternKey SettingCommon Mistake
Reverse proxyproxy_set_header Host $hostMissing β€” breaks URL generation
SSL/TLSssl_protocols TLSv1.2 TLSv1.3Leaving TLSv1.0/1.1 enabled
Gzipgzip_types explicit listCompressing images (waste of CPU)
Cachingimmutable for hashed assetsShort-caching immutable files
Rate limitingburst + nodelayNo burst allowance β†’ 429 on normal traffic
Load balancingleast_conn for mixed loadsDefault round-robin for long requests
WebSocketsUpgrade + Connection headersWS connections drop immediately

Nginx configuration is mostly about understanding these patterns, their interaction order, and the subtle flag differences that change behavior completely. Once these building blocks are solid, you can confidently configure any web infrastructure scenario.

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.