Nginx is everywhere—reverse proxy, load balancer, static file server, SSL terminator. Here are the configuration patterns that work in production.
Base Configuration#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| # /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time';
access_log /var/log/nginx/access.log main;
# Performance
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json
application/javascript application/xml+rss
application/atom+xml image/svg+xml;
# Security
server_tokens off;
include /etc/nginx/conf.d/*.conf;
}
|
Reverse Proxy#
Basic Proxy#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| # /etc/nginx/conf.d/app.conf
upstream backend {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://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 Connection "";
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
|
Load Balancing#
1
2
3
4
5
6
7
8
9
| upstream backend {
least_conn; # or: round_robin, ip_hash, hash $request_uri
server backend1.local:3000 weight=3;
server backend2.local:3000 weight=2;
server backend3.local:3000 backup;
keepalive 32;
}
|
Health Checks (Commercial)#
1
2
3
4
5
6
7
| # nginx plus only
upstream backend {
server backend1:3000;
server backend2:3000;
health_check interval=5s fails=3 passes=2;
}
|
SSL/TLS Configuration#
Modern SSL Setup#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# Modern 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;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Session caching
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# ... rest of config
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
|
Let’s Encrypt with Certbot#
1
2
3
4
5
6
7
8
9
10
11
12
| server {
listen 80;
server_name example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$server_name$request_uri;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| server {
# ... SSL config ...
# 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 Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
# HSTS (be careful - hard to undo)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Hide server version
server_tokens off;
}
|
Rate Limiting#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # Define rate limit zones
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
# General rate limit
limit_req zone=general burst=20 nodelay;
limit_conn addr 10;
location /api/login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://backend;
}
location /api/ {
limit_req zone=general burst=50 nodelay;
proxy_pass http://backend;
}
}
|
Rate Limit Response#
1
2
3
4
5
6
7
8
9
| limit_req_status 429;
limit_conn_status 429;
error_page 429 /429.json;
location = /429.json {
internal;
default_type application/json;
return 429 '{"error": "Too many requests"}';
}
|
Caching#
Proxy Cache#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m
max_size=1g inactive=60m use_temp_path=off;
server {
location /api/ {
proxy_cache app_cache;
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
}
}
|
Static File Caching#
1
2
3
4
5
6
7
8
9
10
11
| location /static/ {
alias /var/www/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
expires 30d;
add_header Cache-Control "public";
}
|
WebSocket Support#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
}
|
API Gateway Patterns#
Path-Based Routing#
1
2
3
4
5
6
7
8
9
10
11
12
13
| server {
location /api/users {
proxy_pass http://users-service;
}
location /api/orders {
proxy_pass http://orders-service;
}
location /api/inventory {
proxy_pass http://inventory-service;
}
}
|
API Versioning#
1
2
3
4
5
6
7
8
9
10
11
12
| location /api/v1/ {
proxy_pass http://api-v1/;
}
location /api/v2/ {
proxy_pass http://api-v2/;
}
# Default to latest
location /api/ {
proxy_pass http://api-v2/;
}
|
Request/Response Modification#
1
2
3
4
5
6
7
8
9
10
11
| location /api/ {
# Add headers to upstream request
proxy_set_header X-Request-ID $request_id;
proxy_set_header X-Forwarded-Host $host;
# Modify response headers
proxy_hide_header X-Powered-By;
add_header X-Request-ID $request_id;
proxy_pass http://backend;
}
|
Static Site Hosting#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| server {
listen 80;
server_name example.com;
root /var/www/html;
index index.html;
# SPA routing - try file, then directory, then fall back to index
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
# Custom error pages
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
}
|
Debugging#
Debug Logging#
1
2
3
4
5
6
7
| error_log /var/log/nginx/error.log debug;
# Or per-location
location /api/ {
error_log /var/log/nginx/api-debug.log debug;
# ...
}
|
Request Inspection#
1
2
3
4
| location /debug {
default_type text/plain;
return 200 "Request: $request\nHost: $host\nURI: $uri\nArgs: $args\nRemote: $remote_addr\n";
}
|
Test Configuration#
1
2
3
4
| nginx -t # Test config syntax
nginx -T # Test and dump full config
nginx -s reload # Reload configuration
nginx -s reopen # Reopen log files
|
Complete Production Example#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| upstream api {
least_conn;
server api1:3000 weight=2;
server api2:3000 weight=2;
server api3:3000 backup;
keepalive 32;
}
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:10m max_size=1g;
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
# Security
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000" always;
# Static files
location /static/ {
alias /var/www/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# API
location /api/ {
limit_req zone=api burst=50 nodelay;
proxy_cache api_cache;
proxy_cache_valid 200 1m;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://api;
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 Connection "";
}
# SPA
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}
}
|
Nginx configuration is declarative and predictable once you understand the patterns. Start with a solid base, add features incrementally, and always test with nginx -t before reloading. The patterns above cover 95% of production use cases—adapt them to your needs.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.