← Back to Blog DevOps

Nginx Complete Guide: Config, Reverse Proxy, SSL, Rewrites & Performance (2026)

⚙️ Nginx Tools — Build Configs Visually, 100% Client-Side

1. What is Nginx and How Does It Work?

Nginx (pronounced "engine-x") was created by Igor Sysoev in 2002 to solve the C10K problem — handling 10,000 simultaneous connections on a single server. Apache's process-per-request model consumed a new process (or thread) for every connection. At high concurrency, this exhausted RAM and CPU context-switching overhead.

Nginx uses an event-driven, asynchronous, non-blocking architecture. A small fixed number of worker processes each handle thousands of connections using OS-level event notifications (epoll on Linux, kqueue on BSD). One worker can manage 10,000+ simultaneous connections while consuming only megabytes of RAM.

What Nginx Is Used For

  • Web server — serving static files (HTML, CSS, JS, images) at high speed with efficient kernel sendfile() calls.
  • Reverse proxy — sitting in front of Node.js, Python (Gunicorn/Uvicorn), PHP-FPM, or Java backends.
  • SSL terminator — handling TLS encryption/decryption so your backend receives plain HTTP.
  • Load balancer — distributing requests across multiple backend servers.
  • HTTP cache — caching backend responses to reduce load and improve response time.
  • API gateway — rate limiting, auth, routing at the edge.

Nginx vs Apache

FeatureNginxApache
ArchitectureEvent-driven, asyncProcess/thread-per-request
Static file performanceExcellentGood
Memory at high concurrencyVery lowHigh
.htaccess supportNoYes (per-directory overrides)
Module systemCompiled-in (mostly)Dynamic loading
Config styleDeclarative blocksDirective files

2. Nginx Config Structure

The main Nginx configuration file is /etc/nginx/nginx.conf. It uses a hierarchical block structure:

# /etc/nginx/nginx.conf (simplified)
user www-data;
worker_processes auto;      # one per CPU core
pid /run/nginx.pid;

events {
    worker_connections 1024; # max connections per worker
    use epoll;               # Linux event model
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    sendfile on;
    tcp_nopush on;
    keepalive_timeout 65;
    server_tokens off;       # hide Nginx version

    # Logging
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log warn;

    # Include site configs
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

The sites-available / sites-enabled Pattern

Debian/Ubuntu Nginx packages use a convention:

  • /etc/nginx/sites-available/ — all server block config files (active or not).
  • /etc/nginx/sites-enabled/ — symlinks to the files you want active.
# Enable a site:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

# Disable a site (just remove the symlink, the original is safe):
sudo rm /etc/nginx/sites-enabled/example.com

# Always test after changes:
sudo nginx -t
sudo systemctl reload nginx

3. Server Blocks and Virtual Hosts

Server blocks are the equivalent of Apache VirtualHosts. Each server block defines how Nginx responds to a specific domain name (or IP). Nginx selects a server block using the server_name directive matched against the HTTP Host header.

# /etc/nginx/sites-available/example.com
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    # Redirect HTTP → HTTPS (clean pattern)
    return 301 https://$host$request_uri;
}

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

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

    # Strong SSL settings
    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:10m;
    ssl_session_timeout 1d;
    ssl_stapling on;
    ssl_stapling_verify on;

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

    location / {
        try_files $uri $uri/ =404;
    }
}

Use our Nginx Config Generator to build complete server blocks — with SSL, proxy, and performance settings — without writing a single line of config manually.

4. SSL/TLS Setup with Let's Encrypt

Let's Encrypt provides free, automated, domain-validated TLS certificates. The Certbot tool handles certificate issuance, installation, and automatic renewal.

# Install Certbot (Ubuntu/Debian)
sudo apt install certbot python3-certbot-nginx

# Obtain and auto-install certificate for your domain:
sudo certbot --nginx -d example.com -d www.example.com

# Test auto-renewal (renewal runs via systemd timer or cron):
sudo certbot renew --dry-run

# View certificate info:
sudo certbot certificates

Strong TLS Configuration

Certbot installs basic SSL directives. Harden them further with Mozilla's recommended ciphers (see the server block in Section 3) and these additional settings:

# Add to your SSL server block:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;  # Let client pick (modern approach)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;        # Disable for perfect forward secrecy

# OCSP stapling (speeds up TLS handshake)
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

HSTS — Force HTTPS Forever

After confirming HTTPS works correctly, add the HSTS header so browsers always use HTTPS and never attempt HTTP:

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

Warning: HSTS is difficult to reverse. Test thoroughly before enabling preload.

5. Reverse Proxy Configuration

A reverse proxy receives requests from the internet and forwards them to a backend application. Nginx is the de facto standard for this pattern — it handles SSL termination, compression, security headers, and rate limiting at the edge while your backend (Node.js, Django, FastAPI, etc.) handles business logic.

# Reverse proxy for Node.js / Python / any backend
upstream app_backend {
    server 127.0.0.1:3000;
    keepalive 32;            # maintain persistent connections
}

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;

    location / {
        proxy_pass         http://app_backend;
        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"; # WebSocket support
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;
        proxy_buffering    off;              # disable for streaming/SSE
    }
}

Critical Proxy Headers

HeaderWhy It Matters
X-Real-IPPasses the actual client IP to your backend (otherwise backend sees Nginx's IP)
X-Forwarded-ForStandard chain of IPs through proxies (can be spoofed — validate carefully)
X-Forwarded-ProtoTells your backend whether the original request was HTTP or HTTPS
HostPreserves the original Host header for virtual host routing in your backend
Upgrade / ConnectionRequired for WebSocket proxying

6. Return vs Rewrite: When to Use Each

This is one of the most common sources of confusion in Nginx configuration. The short rule:

  • Use return for redirects and simple responses — it is faster, simpler, and ends processing immediately.
  • Use rewrite when you need regex captures or need to change the internal request URI without a client redirect.
# Nginx rewrite and return examples

# 1. Permanent redirect (301) — old URL to new URL
location = /old-page {
    return 301 /new-page;
}

# 2. Redirect all www to non-www
server {
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

# 3. Rewrite with regex capture — /user/123 → /profile?id=123
location ~ ^/user/([0-9]+)$ {
    rewrite ^/user/([0-9]+)$ /profile?id=$1 last;
}

# 4. Strip trailing slash (except root)
rewrite ^/(.*)/$ /$1 permanent;

# 5. Serve static files, fall back to index.html (SPA pattern)
location / {
    try_files $uri $uri/ /index.html;
}

# 6. Block access to hidden files (.git, .env, etc.)
location ~ /. {
    deny all;
    return 404;
}

Use our Nginx Rewrite Generator to describe your redirect or rewrite rule in plain English and get the correct Nginx syntax — with explanations of which directive to use.

The if Directive — Handle with Care

The Nginx community has a famous post titled "if is Evil" — not because if is always wrong, but because many developers use it incorrectly inside location blocks, causing unexpected behavior. Rules:

  • if at the server level (outside location) is generally safe.
  • if inside location blocks should only be used for return and rewrite — nothing else.
  • Never nest if blocks.
  • Never use if to check file existence (-f, -d) when try_files solves the problem cleanly.

7. Location Block Matching Rules

Location blocks match incoming request URIs to config. Understanding the matching order is essential — wrong order is a common source of bugs:

ModifierTypePriorityExample
= /pathExact matchHighest (stops searching)location = /health { return 200; }
^~ /prefixPrefix, no regexHigh (stops regex search)location ^~ /static/ { ... }
~ patternRegex, case-sensitiveMediumlocation ~ \.php$ { ... }
~* patternRegex, case-insensitiveMediumlocation ~* \.(jpg|png)$ { ... }
/prefixPrefix (no modifier)Lowest (longest match wins)location /api/ { ... }

try_files — The SPA Pattern

Single-page applications (React, Vue, Angular) need all routes to serve index.html. Nginx's try_files directive handles this elegantly:

location / {
    # Try real file → real directory → fallback to index.html
    try_files $uri $uri/ /index.html;
}

8. Rate Limiting

Rate limiting protects your application from abuse, credential stuffing, and DoS attacks. Nginx's built-in ngx_http_limit_req_module implements the leaky bucket algorithm:

# Rate limiting — protect login and API endpoints

# Define a zone in the http block (nginx.conf or a shared conf file):
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;

# Apply to specific locations:
location /api/ {
    limit_req zone=api_limit burst=10 nodelay;
    limit_req_status 429;
    proxy_pass http://app_backend;
}

location /auth/login {
    limit_req zone=login_limit burst=2 nodelay;
    limit_req_status 429;
    proxy_pass http://app_backend;
}

Allowlist Trusted IPs

# Allowlist — skip rate limiting for internal/monitoring IPs
geo $limit {
    default 1;
    10.0.0.0/8 0;      # internal network: exempt
    127.0.0.1   0;     # localhost: exempt
}
map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=api_limit:10m rate=30r/m;

9. Security Headers

Security response headers are a low-effort, high-value hardening layer. They tell browsers how to behave when rendering your pages and protect users from common attacks:

# Security headers — add inside server block
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;

# Hide Nginx version from error pages and Server header
server_tokens off;

What Each Header Does

HeaderProtects Against
Strict-Transport-SecuritySSL stripping, forces HTTPS
X-Frame-Options: DENYClickjacking (embedding in iframes)
X-Content-Type-OptionsMIME-type sniffing attacks
Referrer-PolicyLeaking URLs in the Referer header
Permissions-PolicyRestricts browser feature access (camera, mic)
Content-Security-PolicyXSS, data injection (requires tuning per app)

10. Gzip, Caching, and Performance

# Gzip compression (in http block of nginx.conf)
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
    text/plain text/css text/xml application/json
    application/javascript application/rss+xml
    application/atom+xml image/svg+xml font/woff2;

# Static file caching with far-future expires
location ~* .(css|js|woff2|woff|ico|png|jpg|jpeg|gif|svg|webp)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}

# HTML — short cache (content changes)
location ~* .html$ {
    expires 5m;
    add_header Cache-Control "public, must-revalidate";
}

Worker Processes and Connections

# nginx.conf — tune for your CPU and traffic
worker_processes auto;           # matches CPU cores
worker_rlimit_nofile 65535;      # max open files per worker

events {
    worker_connections 4096;     # connections per worker (= total: workers × 4096)
    multi_accept on;
    use epoll;
}

http {
    keepalive_timeout 30;        # close idle keep-alive connections after 30s
    keepalive_requests 100;      # max requests per keep-alive connection
    sendfile on;                 # zero-copy file sending via kernel
    tcp_nopush on;               # batch TCP packets
    tcp_nodelay on;              # send small packets immediately (for proxied)
}

Open File Cache

# Cache file descriptors for static files (avoid repeated open() syscalls)
open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;

11. Troubleshooting and Debugging

ProblemDiagnosisFix
502 Bad GatewayBackend not running or wrong portCheck proxy_pass address; verify backend is running
504 Gateway TimeoutBackend too slowIncrease proxy_read_timeout; optimize backend
413 Request Entity Too LargeUpload too largeAdd client_max_body_size 10m; in server/location block
Config reload fails silentlySyntax errorRun nginx -t before reload
Wrong location block matchesMatching order confusionAdd debug logging; check modifier priority table above
WebSocket 400 errorsMissing Upgrade headersAdd proxy_set_header Upgrade $http_upgrade;

Useful Commands

# Test configuration syntax:
sudo nginx -t

# Reload without downtime:
sudo systemctl reload nginx

# Restart (drops connections — avoid in production):
sudo systemctl restart nginx

# View real-time error log:
sudo tail -f /var/log/nginx/error.log

# View access log:
sudo tail -f /var/log/nginx/access.log

# Check which config file an nginx option is set in:
sudo nginx -T | grep option-name

# Show active connections:
nginx_status (if ngx_http_stub_status_module is enabled)

Frequently Asked Questions

What is Nginx and what is it used for?
Nginx is a high-performance web server, reverse proxy, load balancer, and HTTP cache. It uses an event-driven, non-blocking architecture that handles tens of thousands of simultaneous connections with low memory usage. It is used as a web server for static files, a reverse proxy in front of application servers, an SSL terminator, and a load balancer.
What is the difference between nginx.conf and sites-enabled?
nginx.conf is the main configuration file containing global settings and the http block. The http block includes files from sites-enabled/* which are symlinks to site-specific server block configs stored in sites-available. This pattern lets you enable and disable sites by creating or removing symlinks without deleting config files.
What is the difference between Nginx return and rewrite?
return immediately terminates processing and sends an HTTP response — use it for redirects and simple responses. It is faster and simpler. rewrite modifies the request URI using a PCRE regex and can capture groups for substitution. Use rewrite when you need to transform URLs internally or extract parts of the URL into query parameters.
How do I set up Nginx as a reverse proxy for Node.js?
Create a server block with your domain's server_name, then add a location block with proxy_pass http://127.0.0.1:3000. Add proxy_set_header directives for Host, X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto. Use Certbot for SSL. Your Node.js app runs on port 3000 internally; Nginx handles the internet-facing traffic.
How do I test my Nginx config without restarting?
Run 'nginx -t' to validate the configuration syntax without applying changes. If it reports "test is successful", run 'systemctl reload nginx' to gracefully apply changes without dropping existing connections. Never use restart unless reload fails.
How do I redirect HTTP to HTTPS in Nginx?
Add a server block for port 80 that immediately returns a 301 redirect: "return 301 https://$host$request_uri;". Your HTTPS server block listens on port 443 with ssl_certificate and ssl_certificate_key. Avoid using rewrite for this — return 301 is simpler and slightly faster.