Nginx Configuration Guide: From Beginner to Production (2026)
Nginx powers over 30% of all websites and serves some of the biggest platforms on the internet. Whether you're deploying a simple static site or a complex microservices architecture, Nginx is likely part of your stack.
This guide takes you from zero to production-ready Nginx configuration.
Nginx Basics: How It Works
Nginx is an event-driven web server and reverse proxy. Unlike Apache (which creates a thread per connection), Nginx uses an asynchronous event loop. This makes it incredibly efficient at handling thousands of concurrent connections.
Key Concepts
- Server block: A virtual server configuration (like Apache's VirtualHost)
- Location block: Defines how to handle specific URL paths
- Upstream: A backend server group for load balancing
- Directive: A configuration setting (like
listen,root,proxy_pass)
Configuration File Structure
/etc/nginx/
├── nginx.conf # Main configuration
├── sites-available/ # Available site configs
│ ├── default
│ └── myapp.conf
├── sites-enabled/ # Enabled sites (symlinks to sites-available)
│ └── default -> /etc/nginx/sites-available/default
├── conf.d/ # Additional configurations
└── snippets/ # Reusable config snippets
Installing Nginx
# Ubuntu/Debian
sudo apt update && sudo apt install nginx
# CentOS/RHEL/Fedora
sudo dnf install nginx
# Verify installation
nginx -v
systemctl status nginx
Basic Server Block
The simplest Nginx configuration:
server {
listen 80;
server_name example.com;
root /var/www/html;
index index.html;
}
This serves static files from /var/www/html on port 80 for requests to example.com.
Serving a Static Website (Production-Ready)
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript
application/javascript application/json
application/xml application/xml+rss image/svg+xml;
# Static file caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML files — short cache
location ~* \.html$ {
expires 1h;
add_header Cache-Control "public, must-revalidate";
}
# 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;
# Try static files, then fall back to 404
location / {
try_files $uri $uri/ =404;
}
# Custom 404 page
error_page 404 /404.html;
location = /404.html {
internal;
}
}
Reverse Proxy Setup
The most common Nginx use case — proxying requests to a backend application:
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
Proxying Multiple Services
server {
listen 80;
server_name app.example.com;
# Frontend (Next.js/React)
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# API backend
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# WebSocket server
location /ws/ {
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
SSL/HTTPS with Let's Encrypt
Install Certbot
sudo apt install certbot python3-certbot-nginx
Get SSL Certificate
# Automatic — Certbot modifies Nginx config for you
sudo certbot --nginx -d example.com -d www.example.com
# Or manual (if you want to configure SSL yourself)
sudo certbot certonly --nginx -d example.com
Manual SSL Configuration
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# SSL optimization
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
# Modern TLS configuration
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;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
root /var/www/example;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
Auto-Renewal
# Test renewal
sudo certbot renew --dry-run
# Certbot adds a cron job automatically. Verify:
sudo systemctl list-timers | grep certbot
Load Balancing
Nginx can distribute traffic across multiple backend servers:
upstream backend {
# Round-robin (default)
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
# Or weighted
# server 127.0.0.1:3000 weight=3;
# server 127.0.0.1:3001 weight=1;
# Or least connections
# least_conn;
# Or IP hash (sticky sessions)
# ip_hash;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Health Checks
upstream backend {
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
}
If a server fails 3 times within 30 seconds, Nginx marks it as unavailable for 30 seconds.
Caching
Proxy Cache
Cache responses from backend APIs:
# Define cache zone
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m
max_size=1g inactive=60m use_temp_path=off;
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_cache api_cache;
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_key "$scheme$request_method$host$request_uri";
# Add header to see if cache hit/miss
add_header X-Cache-Status $upstream_cache_status;
}
}
FastCGI Cache (for PHP-FPM)
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=php_cache:10m
max_size=1g inactive=60m;
server {
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_cache php_cache;
fastcgi_cache_valid 200 10m;
include fastcgi_params;
}
}
Rate Limiting
Protect your server from abuse:
# Define rate limit zone
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
}
This allows 10 requests per second per IP, with a burst capacity of 20. Excess requests get a 503 response.
File Upload Size
The default upload limit is 1MB. For most applications, you need more:
server {
client_max_body_size 50M;
location /upload/ {
client_max_body_size 500M; # Even larger for uploads
proxy_pass http://127.0.0.1:3000;
}
}
Performance Tuning
Worker Configuration (/etc/nginx/nginx.conf)
# Auto-detect number of CPU cores
worker_processes auto;
# Max connections per worker
events {
worker_connections 1024;
multi_accept on;
}
http {
# Sendfile for static files
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# Keepalive
keepalive_timeout 65;
keepalive_requests 100;
# Buffer sizes
client_body_buffer_size 10K;
client_header_buffer_size 1k;
large_client_header_buffers 2 1k;
}
System-Level Limits
# Increase file descriptor limit
sudo sysctl -w fs.file-max=65535
# Add to /etc/security/limits.conf
# * soft nofile 65535
# * hard nofile 65535
Debugging Nginx
Test Configuration
# Test without restarting
sudo nginx -t
# Test and show the resolved configuration
sudo nginx -T | less
Check Error Logs
# Real-time error log
sudo tail -f /var/log/nginx/error.log
# Access log
sudo tail -f /var/log/nginx/access.log
Common Errors
502 Bad Gateway: Backend is down or unreachable. Check if your app is running.
504 Gateway Timeout: Backend is too slow. Increase timeout:
proxy_read_timeout 300;
proxy_connect_timeout 300;
413 Request Entity Too Large: File upload too big. Increase client_max_body_size.
Too many open files: Increase worker_rlimit_nofile in nginx.conf and system file limits.
Security Hardening Checklist
- Hide Nginx version:
server_tokens off;
- Disable unused HTTP methods:
if ($request_method !~ ^(GET|POST|HEAD|PUT|DELETE)$ ) {
return 405;
}
- Block dotfiles:
location ~ /\. {
deny all;
}
- Add security headers:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=63072000" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'" always;
- Use fail2ban to block brute-force attacks:
sudo apt install fail2ban
sudo systemctl enable fail2ban
Conclusion
Nginx is powerful but its configuration can be intimidating. The key is starting simple and adding complexity as needed:
- Start with a basic server block serving static files
- Add SSL with Let's Encrypt
- Set up reverse proxy for your backend applications
- Add caching, rate limiting, and security headers
- Tune performance when you have real traffic data
Bookmark this guide and use it as a reference. Every configuration block here is production-tested and ready to use.
The Nginx docs at nginx.org/en/docs are excellent when you need to go deeper. But for 90% of use cases, what's in this guide is all you need.