- Nginx Config Generator — build full server blocks with a visual form
- Nginx Rewrite Generator — generate correct return/rewrite rules from descriptions
- Systemd Generator — create nginx.service or app service unit files
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
| Feature | Nginx | Apache |
|---|---|---|
| Architecture | Event-driven, async | Process/thread-per-request |
| Static file performance | Excellent | Good |
| Memory at high concurrency | Very low | High |
| .htaccess support | No | Yes (per-directory overrides) |
| Module system | Compiled-in (mostly) | Dynamic loading |
| Config style | Declarative blocks | Directive 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
| Header | Why It Matters |
|---|---|
X-Real-IP | Passes the actual client IP to your backend (otherwise backend sees Nginx's IP) |
X-Forwarded-For | Standard chain of IPs through proxies (can be spoofed — validate carefully) |
X-Forwarded-Proto | Tells your backend whether the original request was HTTP or HTTPS |
Host | Preserves the original Host header for virtual host routing in your backend |
Upgrade / Connection | Required 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
returnfor redirects and simple responses — it is faster, simpler, and ends processing immediately. - Use
rewritewhen 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:
ifat the server level (outside location) is generally safe.ifinside location blocks should only be used forreturnandrewrite— nothing else.- Never nest
ifblocks. - Never use
ifto check file existence (-f,-d) whentry_filessolves 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:
| Modifier | Type | Priority | Example |
|---|---|---|---|
= /path | Exact match | Highest (stops searching) | location = /health { return 200; } |
^~ /prefix | Prefix, no regex | High (stops regex search) | location ^~ /static/ { ... } |
~ pattern | Regex, case-sensitive | Medium | location ~ \.php$ { ... } |
~* pattern | Regex, case-insensitive | Medium | location ~* \.(jpg|png)$ { ... } |
/prefix | Prefix (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
| Header | Protects Against |
|---|---|
| Strict-Transport-Security | SSL stripping, forces HTTPS |
| X-Frame-Options: DENY | Clickjacking (embedding in iframes) |
| X-Content-Type-Options | MIME-type sniffing attacks |
| Referrer-Policy | Leaking URLs in the Referer header |
| Permissions-Policy | Restricts browser feature access (camera, mic) |
| Content-Security-Policy | XSS, 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
| Problem | Diagnosis | Fix |
|---|---|---|
| 502 Bad Gateway | Backend not running or wrong port | Check proxy_pass address; verify backend is running |
| 504 Gateway Timeout | Backend too slow | Increase proxy_read_timeout; optimize backend |
| 413 Request Entity Too Large | Upload too large | Add client_max_body_size 10m; in server/location block |
| Config reload fails silently | Syntax error | Run nginx -t before reload |
| Wrong location block matches | Matching order confusion | Add debug logging; check modifier priority table above |
| WebSocket 400 errors | Missing Upgrade headers | Add 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.