Toolmingo
Guides10 min read

Nginx Cheat Sheet: Config, Reverse Proxy, SSL, and More

A complete nginx cheat sheet — server blocks, reverse proxy, SSL/TLS, gzip, caching, rate limiting, redirects, and debugging. Copy-ready configs for real-world setups.

Nginx is the most-used web server and reverse proxy on the internet. This reference covers every config block you'll reach for — from basic serving to SSL termination, upstream load balancing, and performance tuning.

Quick reference

The 25 patterns that cover 95% of nginx work.

Task Key directive
Serve static files root /var/www/html; try_files $uri $uri/ =404;
Listen on port 80 listen 80;
Server name server_name example.com www.example.com;
HTTPS redirect return 301 https://$host$request_uri;
SSL certificate ssl_certificate /etc/ssl/cert.pem;
Reverse proxy proxy_pass http://localhost:3000;
Set proxy headers proxy_set_header Host $host;
Gzip compression gzip on; gzip_types text/plain application/json;
Cache static files expires 1y; add_header Cache-Control "public";
Rate limit zone limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
Apply rate limit limit_req zone=api burst=20 nodelay;
Rewrite URL rewrite ^/old/(.*)$ /new/$1 permanent;
Custom error page error_page 404 /404.html;
Return 403 return 403;
Upstream group upstream backend { server 127.0.0.1:3000; }
Basic auth auth_basic "Private"; auth_basic_user_file /etc/nginx/.htpasswd;
Client max body client_max_body_size 50M;
Keepalive timeout keepalive_timeout 65;
Worker processes worker_processes auto;
Test config nginx -t
Reload without downtime nginx -s reload
View error log tail -f /var/log/nginx/error.log
Block IP deny 192.168.1.1; allow all;
Enable site (Debian) ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
Disable default site rm /etc/nginx/sites-enabled/default

CLI commands

# Check nginx version
nginx -v
nginx -V   # includes compiled modules

# Test configuration for syntax errors
nginx -t
nginx -T   # test + print full merged config

# Start / stop / reload
systemctl start nginx
systemctl stop nginx
systemctl reload nginx   # graceful reload — no dropped connections
systemctl restart nginx  # full restart (drops connections briefly)

# Send signals directly
nginx -s reload    # graceful reload
nginx -s quit      # graceful shutdown
nginx -s stop      # immediate shutdown

# View logs live
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log

Config file structure

/etc/nginx/
├── nginx.conf           # main config (http block, includes)
├── conf.d/              # global snippets
├── sites-available/     # all server blocks (Debian/Ubuntu)
└── sites-enabled/       # symlinks to active sites

nginx.conf skeleton:

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
    multi_accept on;
}

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

    sendfile    on;
    tcp_nopush  on;
    tcp_nodelay on;

    keepalive_timeout 65;
    server_tokens off;   # hide nginx version

    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Server blocks

A server block is nginx's equivalent of an Apache VirtualHost.

server {
    listen 80;
    listen [::]:80;           # IPv6
    server_name example.com www.example.com;

    root /var/www/example;
    index index.html index.php;

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

try_files explained:

try_files $uri $uri/ =404;
# 1. Try the exact URI as a file
# 2. Try the URI as a directory (look for index)
# 3. Return 404 if neither found

try_files $uri $uri/ /index.php?$query_string;
# SPA / WordPress: fall through to index.php

Reverse proxy

Forward requests to a backend app (Node.js, Python, etc.).

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

    location / {
        proxy_pass http://127.0.0.1:3000;

        # Essential 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;

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

        # Buffering (disable for SSE/streaming)
        proxy_buffering off;
    }
}

WebSocket proxying:

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";
}

SSL / TLS (Let's Encrypt)

Install Certbot, then let it generate the config:

apt install certbot python3-certbot-nginx
certbot --nginx -d example.com -d www.example.com
# Certbot edits your nginx config and sets up auto-renewal

Manual SSL server block:

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_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

    # Modern TLS 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;

    add_header Strict-Transport-Security "max-age=63072000" always;

    root /var/www/example;
    # ... rest of your location blocks
}

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

Location blocks

Locations match request URIs. Matching order: exact (=) → prefix (^~) → regex (~, ~*) → longest prefix.

# Exact match (highest priority)
location = /favicon.ico {
    access_log off;
    log_not_found off;
}

# Prefix match (no regex fallback)
location ^~ /images/ {
    root /var/www/static;
    expires 30d;
}

# Case-sensitive regex
location ~ \.php$ {
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

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

# Prefix fallback (most general)
location / {
    try_files $uri $uri/ =404;
}

Redirects and rewrites

# Permanent redirect (301)
return 301 https://example.com$request_uri;
return 301 /new-page;

# Temporary redirect (302)
return 302 /maintenance;

# Rewrite path (internal, no browser redirect)
rewrite ^/blog/(.+)$ /posts/$1 last;

# Rewrite with redirect (browser sees new URL)
rewrite ^/old-page$ /new-page permanent;

# Strip trailing slash
rewrite ^/(.*)/$ /$1 redirect;

# Non-www → www
server {
    listen 80;
    server_name example.com;
    return 301 http://www.example.com$request_uri;
}

last vs break vs redirect vs permanent:

Flag Effect
last Stop processing rewrites; re-search locations
break Stop processing rewrites; continue in same location
redirect 302 temporary redirect to client
permanent 301 permanent redirect to client

Load balancing

Distribute traffic across multiple backends.

upstream backend {
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
    server 10.0.0.3:3000 weight=2;  # gets 2x traffic
    server 10.0.0.4:3000 backup;    # only used if others are down
}

server {
    location / {
        proxy_pass http://backend;
    }
}

Load balancing methods:

upstream backend {
    least_conn;   # send to server with fewest active connections
    # ip_hash;    # sticky sessions by client IP
    # random;     # random selection
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
}

Gzip compression

http {
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;         # 1 (fast) to 9 (best compression)
    gzip_min_length 1024;      # don't compress tiny responses
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        image/svg+xml;
    # Note: never gzip binary formats (images, video, zip)
}

Caching

Static file caching (browser):

location ~* \.(css|js)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

location ~* \.(jpg|jpeg|png|gif|ico|webp|svg)$ {
    expires 30d;
    add_header Cache-Control "public";
}

location / {
    add_header Cache-Control "no-cache, must-revalidate";
}

Proxy caching (nginx stores backend responses):

http {
    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m max_size=1g inactive=60m;

    server {
        location /api/ {
            proxy_pass http://backend;
            proxy_cache api_cache;
            proxy_cache_valid 200 10m;
            proxy_cache_valid 404 1m;
            proxy_cache_bypass $http_pragma;
            add_header X-Cache-Status $upstream_cache_status;
        }
    }
}

Rate limiting

http {
    # Define zone: 10MB stores ~160,000 IPs
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
    limit_req_zone $binary_remote_addr zone=api:10m   rate=100r/m;

    server {
        # Strict limit for login endpoint
        location /api/login {
            limit_req zone=login burst=3 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }

        # Generous limit for general API
        location /api/ {
            limit_req zone=api burst=50 nodelay;
            proxy_pass http://backend;
        }
    }
}

burst and nodelay explained:

  • burst=20 — allow 20 queued requests above the rate limit
  • nodelay — process burst requests immediately (no artificial delay)
  • Without nodelay: excess requests are delayed to meet the rate

Security headers

server {
    # Hide nginx version
    server_tokens off;

    # Security headers
    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        "camera=(), microphone=(), geolocation=()" always;

    # Content Security Policy (adjust for your app)
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;

    # HSTS (only after you're sure HTTPS works)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
}

Block IPs and requests

# Block specific IPs
location / {
    deny  192.168.1.100;
    deny  10.0.0.0/8;    # deny entire subnet
    allow all;
}

# Allow only specific IPs (whitelist)
location /admin {
    allow 203.0.113.1;
    deny  all;
}

# Block bad user agents
if ($http_user_agent ~* "(curl|wget|python|scanner|bot)") {
    return 403;
}

# Block requests without a Host header
if ($host = "") {
    return 444;  # nginx closes connection without response
}

PHP-FPM integration

server {
    root /var/www/html;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        # Security: only execute PHP files that exist
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;

        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        # Or TCP: fastcgi_pass 127.0.0.1:9000;

        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Deny access to sensitive files
    location ~ /\.(ht|git|env) {
        deny all;
    }
}

SPA (React, Vue, Angular) hosting

server {
    listen 80;
    server_name app.example.com;
    root /var/www/dist;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
        # Falls back to index.html for all routes
        # Lets the frontend router handle navigation
    }

    # Cache hashed assets forever
    location ~* \.[0-9a-f]{8}\.(js|css)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Common mistakes

Mistake Problem Fix
if ($variable = "value") The if directive in nginx is not like if in code — it has gotchas in location blocks Use map or return instead of if where possible
Forgetting proxy_set_header Host Backend sees nginx host, not client's Host Always set proxy_set_header Host $host;
add_header in child block Headers from parent are silently dropped Repeat all add_header directives in every block that uses them
root vs alias confusion root appends the location path; alias replaces it /static with root /var/www/var/www/static; with alias /var/www/var/www
No nginx -t before reload Syntax error brings down all sites Always nginx -t before nginx -s reload
rewrite … last inside if Can cause infinite redirect loops Use return for simple redirects; reserve rewrite for URL transformation
Not setting worker_processes auto Defaults to 1 worker regardless of CPU cores worker_processes auto; uses all cores

Debugging checklist

# 1. Check syntax
nginx -t

# 2. View error log
tail -100 /var/log/nginx/error.log

# 3. Check if nginx is running
systemctl status nginx

# 4. Confirm listening ports
ss -tlnp | grep nginx

# 5. Test from server itself (bypasses firewall)
curl -I http://localhost
curl -I -k https://localhost   # -k ignores SSL errors

# 6. Verbose connection test
curl -v http://example.com 2>&1 | head -50

# 7. Check which config file is active
nginx -T | head -20

# 8. Test a specific URL rewrite
curl -I http://localhost/old-path    # follow redirects: curl -L

# 9. Firewall check (if curl from outside fails)
ufw status
iptables -L -n | grep -E "80|443"

FAQ

Q: What's the difference between nginx -s reload and systemctl restart nginx?

reload sends a SIGHUP signal: the master process starts new worker processes with the new config, then gracefully drains and kills old workers. No connections are dropped. restart stops nginx entirely and starts fresh — active connections are dropped. Always prefer reload in production.

Q: Why does add_header in a nested location block drop parent headers?

nginx's add_header only inherits from the parent if the child block has no add_header directives of its own. The moment you add one add_header in a location, all parent headers are discarded. Solution: repeat every add_header you need in each block, or use an include file.

# include /etc/nginx/security-headers.conf in every block that needs them

Q: What's the difference between root and alias?

# root: appends the full location path
location /static/ {
    root /var/www;
    # /static/file.css → /var/www/static/file.css
}

# alias: replaces the location path
location /static/ {
    alias /var/www/assets/;
    # /static/file.css → /var/www/assets/file.css
}

Q: How do I enable HTTP/2?

HTTP/2 requires HTTPS. Add http2 to your listen directive:

listen 443 ssl http2;
listen [::]:443 ssl http2;

HTTP/3 (QUIC) requires nginx compiled with --with-http_v3_module (nginx ≥ 1.25):

listen 443 quic reuseport;
add_header Alt-Svc 'h3=":443"; ma=86400';

Q: How do I serve multiple domains from one server?

Create separate server blocks, each with its own server_name. Nginx matches incoming requests by the Host header:

server {
    server_name example.com;
    root /var/www/example;
}

server {
    server_name another.com;
    root /var/www/another;
}

Q: How do I find which server block is handling a request?

Add a temporary header and check the response:

server {
    add_header X-Debug-Server "this-block" always;
}

Then: curl -I http://example.com and look for X-Debug-Server.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools