nginx Configuration — server blocks, reverse proxy and TLS

Practical nginx.conf reference — server and location blocks, reverse proxy, TLS, gzip, caching, rate limiting and security headers with examples.

The nginx.conf is the heart of every nginx server: through directives and nested blocks you define how requests are accepted, routed and served. This reference collects the snippets you reach for daily — from server and location blocks through reverse proxy and TLS to gzip, caching, rate limiting and security headers. Depending on the directive, the examples belong in the http, server or location context of your /etc/nginx/nginx.conf, or in the site files under sites-available/. After every change, validate the config with nginx -t and apply it with a reload — that way a typo never takes your site down. To control the service itself, see the separate nginx CLI cheat sheet.

Config Structure & Contexts

Top-level nginx.conf structure. The http block wraps all web config. include pulls in site files.

# nginx.conf top-level structure
worker_processes auto;
events {
    worker_connections 1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Basic server block (virtual host). Defines which port and domain this block handles.

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/html;
    index index.php index.html;
}

Location blocks match request URIs. Exact (=), prefix (/), case-insensitive regex (~*), and case-sensitive regex (~).

location / {
    try_files $uri $uri/ =404;
}
location /api/ {
    proxy_pass http://localhost:3000/;
}
location ~* \.php$ {
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

NGINX evaluates location blocks in this priority order. Exact matches (=) always win.

# Location match priority (highest to lowest):
# 1. = exact match
# 2. ^~ prefix match (stops regex search)
# 3. ~  case-sensitive regex
# 4. ~* case-insensitive regex
# 5. /  prefix match (longest wins)

Virtual Hosts (HTTP)

Complete basic HTTP virtual host with document root, index files, and separate log files.

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/example;
    index index.php index.html index.htm;
    access_log /var/log/nginx/example-access.log;
    error_log  /var/log/nginx/example-error.log;
    location / {
        try_files $uri $uri/ =404;
    }
}

Default catch-all server block. Drops requests with no matching server_name (return 444 closes connection).

server {
    listen 80 default_server;
    server_name _;
    return 444;
}

Redirect www to non-www (or vice versa) with a permanent 301 redirect.

server {
    listen 80;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

SSL / TLS (HTTPS)

HTTPS server block with Let's Encrypt certificate. TLS 1.2 and 1.3 only, weak ciphers disabled.

server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
}

Redirect all HTTP traffic to HTTPS. Place above the SSL server block.

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

SSL performance and security options. Session cache reduces handshake overhead. OCSP stapling speeds up certificate validation.

ssl_session_cache   shared:SSL:10m;
ssl_session_timeout 1d;
ssl_dhparam /etc/nginx/dhparam.pem;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 1.1.1.1 valid=300s;

listen 443 ssl http2; — Enable HTTP/2 alongside SSL. HTTP/2 requires HTTPS and significantly improves performance. Since nginx 1.25.1, the separate http2 on; directive is recommended.

listen 443 ssl http2;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; — HSTS header: instructs browsers to always use HTTPS for this domain for 1 year.

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

Reverse Proxy

Reverse proxy: forward /api/ requests to a backend app on port 3000. Passes client IP and protocol headers.

location /api/ {
    proxy_pass http://localhost:3000/;
    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;
}

Required headers for WebSocket proxying. Add inside the proxy location block.

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Proxy timeout and buffering settings. Increase timeouts for slow backend applications.

proxy_connect_timeout 60s;
proxy_send_timeout    60s;
proxy_read_timeout    60s;
proxy_buffering       on;
proxy_buffer_size     16k;
proxy_buffers         4 16k;

Enable proxy response caching. Cached responses serve directly from disk, reducing backend load.

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m;
location / {
    proxy_cache my_cache;
    proxy_cache_valid 200 302 10m;
    proxy_cache_valid 404 1m;
    proxy_pass http://backend;
}

PHP-FPM Integration

Pass PHP requests to PHP-FPM via Unix socket. Adjust socket path for your PHP version.

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Pass PHP requests to PHP-FPM via TCP on port 9000. Use TCP when FPM runs on a different host.

location ~ \.php$ {
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include        fastcgi_params;
}

Front controller pattern: route all non-file requests to index.php (required for Laravel, WordPress, etc.).

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

Cache FastCGI (PHP-FPM) responses to reduce PHP execution on repeated requests.

fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=php_cache:10m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
location ~ \.php$ {
    fastcgi_cache php_cache;
    fastcgi_cache_valid 200 5m;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Rewrites & Redirects

return 301 https://example.com$request_uri; — Permanent redirect to a new URL. Faster than rewrite: no regex processing.

return 301 https://example.com$request_uri;

return 302 /maintenance.html; — Temporary redirect to a maintenance page.

return 302 /maintenance.html;

Rewrite rules: 'permanent' = 301, 'redirect' = 302. Use return when possible; rewrite for pattern matching.

rewrite ^/old-page$ /new-page permanent;
rewrite ^/blog/(.*)$ /posts/$1 redirect;

rewrite ^/index\.php/?(.*)$ /$1 permanent; — Remove index.php from all URLs with a permanent redirect.

rewrite ^/index\.php/?(.*)$ /$1 permanent;

Strip www from any domain using a regex capture group. Prefer a separate server block for this.

if ($host ~* ^www\.(.+)$) {
    return 301 https://$1$request_uri;
}

Custom error pages. The location block serves the error page from a specific path.

error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
    root /var/www/html;
}

Gzip Compression

Enable gzip compression for common text-based content types. Place inside the http block.

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml;

gzip_disable "msie6"; — Disable gzip for Internet Explorer 6 (buggy gzip implementation). Safe to include.

gzip_disable "msie6";

gunzip on; — Decompress gzip responses from upstream before passing to clients that don't support gzip.

gunzip on;

Static Files & Browser Caching

Cache images for 30 days in the browser. 'immutable' tells the browser not to revalidate.

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

Cache CSS and JS for 1 year. Works best with versioned/hashed filenames.

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

Long-term caching for web fonts. CORS header required when fonts are loaded cross-origin.

location ~* \.(woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public";
    add_header Access-Control-Allow-Origin "*";
}

Suppress 404 log entries for favicon.ico and robots.txt (very common requests).

location = /favicon.ico {
    log_not_found off;
    access_log off;
}
location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
}

Performance basics for the http block. sendfile enables kernel-level file transfer. keepalive reduces connection overhead.

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

Security Headers

Essential security headers. 'always' ensures the header is added even on error responses.

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 Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; — Content Security Policy header. Restricts what resources the browser can load. Adjust sources to fit your app.

add_header Content-Security-Policy "default-src 'self';" always;

add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; — Permissions Policy: disable camera, microphone, and geolocation APIs for this site.

add_header Permissions-Policy "camera=(), microphone=()" always;

server_tokens off; — Hide the NGINX version number from HTTP response headers and error pages.

server_tokens off;

Block access to hidden files and directories (e.g. .git, .env, .htaccess). Allows .well-known for ACME challenges.

location ~ /\.(?!well-known) {
    deny all;
}

Block direct access to sensitive file types (backups, configs, logs, etc.).

location ~* \.(bak|conf|dist|fla|inc|ini|log|psd|sh|sql|swp)$ {
    deny all;
}

Rate Limiting & Access Control

Rate limiting: allow max 10 requests/second per IP, bursting up to 20. Excess requests get 503.

# In http block:
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s;
# In server or location block:
limit_req zone=req_limit burst=20 nodelay;

Limit the number of concurrent connections per IP to 10.

limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
limit_conn conn_limit 10;

Whitelist specific IPs or subnets and deny all others. Useful for admin areas.

allow 192.168.1.0/24;
allow 10.0.0.1;
deny all;

HTTP Basic Authentication. Generate .htpasswd with: htpasswd -c /etc/nginx/.htpasswd username

auth_basic "Restricted Area";
auth_basic_user_file /etc/nginx/.htpasswd;

client_max_body_size 50M; — Set the maximum allowed size of the client request body. Increase for file uploads.

client_max_body_size 50M;

Load Balancing

Round-robin load balancing across three backend servers. Requests are distributed evenly.

upstream backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080;
}
server {
    location / {
        proxy_pass http://backend;
    }
}

Least connections load balancing: sends new requests to the server with fewest active connections.

upstream backend {
    least_conn;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

IP hash load balancing: routes the same client IP always to the same backend (session persistence).

upstream backend {
    ip_hash;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

Weighted load balancing with a backup server. The backup only receives requests when primary servers are down.

upstream backend {
    server 192.168.1.10:8080 weight=3;
    server 192.168.1.11:8080 weight=1;
    server 192.168.1.12:8080 backup;
}

Passive health checks: mark a server as failed after 3 failed attempts, then skip it for 30 seconds.

upstream backend {
    server 192.168.1.10:8080 max_fails=3 fail_timeout=30s;
    server 192.168.1.11:8080 max_fails=3 fail_timeout=30s;
}

Logging

Define a custom log format named 'main' in the http block. This is the combined log format.

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent" "$http_x_forwarded_for"';

Enable access logging with the 'main' format and error logging at warn level.

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

access_log off; — Disable access logging for a server or location block (useful for static assets).

access_log off;

error_log /var/log/nginx/error.log debug; — Set error log level to debug for verbose output. Levels: debug, info, notice, warn, error, crit, alert, emerg.

error_log /var/log/nginx/error.log debug;

JSON-formatted access log. Useful for log aggregation tools (ELK stack, Loki, etc.).

log_format json_combined escape=json
  '{"time": "$time_local", "remote_addr": "$remote_addr", '
  '"request": "$request", "status": $status, '
  '"bytes": $body_bytes_sent, "ua": "$http_user_agent"}';

Variables Reference

$host — The request's Host header value (or server_name if no Host header is present).

proxy_set_header Host $host;

$request_uri — The full original request URI including query string, as sent by the client.

return 301 https://example.com$request_uri;

$uri — The current (possibly rewritten) request URI without query string.

try_files $uri $uri/ /index.php;

$args / $query_string — The query string part of the request URI (both variables are equivalent).

proxy_pass http://backend?$args;

$remote_addr — The IP address of the connecting client.

add_header X-Client-IP $remote_addr;

$scheme — The request scheme: 'http' or 'https'.

proxy_set_header X-Forwarded-Proto $scheme;

$server_name — The server_name of the server block that matched the request.

add_header X-Served-By $server_name;

$http_<header> — Access any request header. Hyphens become underscores: $http_user_agent, $http_x_forwarded_for.

if ($http_user_agent ~* 'bot') { return 403; }

Conclusion

A well-considered nginx configuration determines the performance, security and stability of your site. Start with a clean server block, add TLS and security headers, and enable gzip and browser caching — then protect public endpoints against abuse with rate limiting. Test every change with nginx -t before reloading, and keep your TLS settings current and deliberate.

Further Reading

  • apache – the classic web server with modular configuration
  • caddy – modern web server with automatic HTTPS
  • certbot – issue and renew Let's Encrypt certificates