Nginx — Reverse Proxy, Load Balancer, and SSL Configuration Guide

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Introduction

Why This Matters

Nginx handles over 400 million websites and is the leading web server and reverse proxy. Its event-driven, non-blocking architecture allows it to handle tens of thousands of concurrent connections with minimal memory. Understanding Nginx configuration enables you to terminate SSL, load balance across backend services, cache responses, apply rate limiting, and serve static assets efficiently — all in a single, battle-tested process.

Installation and Basic Setup

# Install on Ubuntu/Debian
sudo apt update && sudo apt install nginx -y
 
# Install on CentOS/RHEL
sudo yum install epel-release && sudo yum install nginx -y
 
# Start and enable
sudo systemctl start nginx
sudo systemctl enable nginx
 
# Test configuration (always before reload)
sudo nginx -t
 
# Reload without dropping connections
sudo systemctl reload nginx
 
# Key file locations
# /etc/nginx/nginx.conf          — main config
# /etc/nginx/conf.d/*.conf       — site configs (included by main)
# /etc/nginx/sites-available/    — Debian-style site configs
# /var/log/nginx/access.log      — access logs
# /var/log/nginx/error.log       — error logs

Reverse Proxy Configuration

# /etc/nginx/conf.d/my-app.conf
 
upstream backend {
    server 127.0.0.1:3000;
}
 
server {
    listen 80;
    server_name example.com www.example.com;
 
    # Redirect HTTP to HTTPS
    return 301 https://$host$request_uri;
}
 
server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
 
    # SSL configuration (Let's Encrypt)
    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_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
 
    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options DENY always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
 
    location / {
        proxy_pass         http://backend;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection 'upgrade';
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 60s;
        proxy_connect_timeout 10s;
    }
 
    # Serve static files directly (bypass Node.js)
    location /static/ {
        alias /var/www/my-app/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
}

Load Balancing

upstream api_servers {
    # Load balancing methods:
    # round-robin (default) — distributes evenly
    # least_conn            — sends to server with fewest connections
    # ip_hash               — sticky sessions by client IP
 
    least_conn;
 
    server app1.internal:3000 weight=3;
    server app2.internal:3000 weight=3;
    server app3.internal:3000 weight=2;
 
    # Health checking
    server backup.internal:3000 backup;
 
    # Connection limits
    keepalive 32;
}
 
server {
    listen 80;
 
    location /api/ {
        proxy_pass http://api_servers;
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
        proxy_next_upstream_tries 3;
    }
}

Rate Limiting

# Define rate limit zones in http block
http {
    # Limit by IP: 10 requests per second, 1MB zone
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
 
    # Limit by API key header
    limit_req_zone $http_x_api_key zone=api_key_limit:10m rate=100r/s;
 
    server {
        location /api/ {
            # Allow burst of 20, queue up to 20 requests
            limit_req zone=api_limit burst=20 nodelay;
            limit_req_status 429;
 
            proxy_pass http://backend;
        }
    }
}

Caching

http {
    # Proxy cache configuration
    proxy_cache_path /var/cache/nginx
        levels=1:2
        keys_zone=my_cache:10m
        max_size=1g
        inactive=60m
        use_temp_path=off;
 
    server {
        location /api/public/ {
            proxy_cache            my_cache;
            proxy_cache_key        "$scheme$request_method$host$request_uri";
            proxy_cache_valid      200 5m;
            proxy_cache_valid      404 1m;
            proxy_cache_use_stale  error timeout updating http_500 http_502 http_503;
            add_header             X-Cache-Status $upstream_cache_status;
 
            proxy_pass http://backend;
        }
    }
}

Gzip Compression

http {
    gzip              on;
    gzip_comp_level   6;
    gzip_min_length   1000;
    gzip_proxied      any;
    gzip_vary         on;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        image/svg+xml;
}

Let's Encrypt with Certbot

# Install Certbot
sudo apt install certbot python3-certbot-nginx -y
 
# Obtain and install certificate
sudo certbot --nginx -d example.com -d www.example.com
 
# Auto-renewal (cron or systemd timer)
sudo certbot renew --dry-run
 
# Certbot adds a systemd timer — verify it
systemctl status certbot.timer

Common Mistakes

  • Not running nginx -t before systemctl reload nginx — invalid config causes Nginx to fail to load, dropping all traffic
  • Using proxy_pass http://backend/; with trailing slash when location has no trailing slash — causes path rewriting bugs
  • Missing proxy_set_header Host $host — backend receives wrong Host header, breaking virtual hosting
  • Not tuning worker_processes and worker_connections — defaults are conservative; production needs tuning
  • Forgetting add_header with always parameter — headers only send on 2xx responses without always

Best Practices

  • Set worker_processes auto to match CPU count and worker_connections 4096 per worker
  • Enable http2 on SSL listeners for multiplexing and header compression
  • Use proxy_cache_use_stale to serve stale cache during backend failures (graceful degradation)
  • Configure access_log with buffering (buffer=16k flush=5s) to reduce disk I/O
  • Use Nginx error pages to return JSON for API endpoints instead of HTML error pages
  • Monitor Nginx with the stub_status module and integrate with Prometheus via nginx-prometheus-exporter

Key Takeaways

  • Nginx uses an event-driven, non-blocking architecture — one worker process handles thousands of concurrent connections
  • upstream blocks define backend pools; load balancing methods include round-robin, least connections, and IP hash
  • Always run nginx -t to validate configuration before reloading — invalid config prevents reload without downtime
  • Rate limiting with limit_req_zone protects backends from traffic spikes and brute-force attacks
  • Proxy caching with proxy_cache_path reduces backend load for idempotent GET responses
  • SSL termination at Nginx offloads TLS handshake CPU from application servers
  • Security headers (HSTS, X-Content-Type-Options, X-Frame-Options) should be added at the proxy layer, not the application
  • Let's Encrypt with Certbot provides free, auto-renewing TLS certificates with Nginx integration

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading