DevOps

Nginx Configuration Guide: From Beginner to Production (2026)

2026-07-05·12 min read
#Nginx#DevOps#reverse proxy#SSL#performance

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

  1. Hide Nginx version:
server_tokens off;
  1. Disable unused HTTP methods:
if ($request_method !~ ^(GET|POST|HEAD|PUT|DELETE)$ ) {
    return 405;
}
  1. Block dotfiles:
location ~ /\. {
    deny all;
}
  1. 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;
  1. 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:

  1. Start with a basic server block serving static files
  2. Add SSL with Let's Encrypt
  3. Set up reverse proxy for your backend applications
  4. Add caching, rate limiting, and security headers
  5. 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.