Back to skills
SKILL.md
Nginx Expert
BSecurityExpert-level Nginx configuration. Use when writing Nginx configs, setting up reverse proxies, load balancing, SSL/TLS termination, rate limiting, caching, virtual hosts, or debugging Nginx issues. Also use when the user mentions 'Nginx', 'server block', 'proxy_pass', 'upstream', 'rate limit', 'SSL certificate', 'Let's Encrypt', '502 bad gateway', or 'nginx.conf'.
- 11 stars
- 0 votes
- 0 copies
- 2 views
- Added September 10, 2026
Works with
Security analysis
89/100- Uses curl or wget to download content
- Creates or modifies cron jobs for persistent execution
npx -y skills add luokai0/ai-agent-skills-by-luo-kai --skill nginx-expert --agent claude-codeAre you the author of Nginx Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/luokai0-nginx-expert)---
author: luo-kai
name: nginx-expert
description: Expert-level Nginx configuration. Use when writing Nginx configs, setting up reverse proxies, load balancing, SSL/TLS termination, rate limiting, caching, virtual hosts, or debugging Nginx issues. Also use when the user mentions 'Nginx', 'server block', 'proxy_pass', 'upstream', 'rate limit', 'SSL certificate', 'Let's Encrypt', '502 bad gateway', or 'nginx.conf'.
license: MIT
metadata:
author: luokai0
version: "1.0"
category: coding
---
# Nginx Expert
You are an expert in Nginx with deep knowledge of configuration, reverse proxying, load balancing, SSL/TLS, performance tuning, and security hardening.
## Before Starting
1. **Use case** — reverse proxy, load balancer, static file server, API gateway?
2. **SSL** — Let's Encrypt, custom cert, or no SSL?
3. **Backend** — Node.js, Python, Go, PHP, static files?
4. **Problem type** — writing config, debugging 502/504, performance, security?
5. **Environment** — bare metal, Docker, Kubernetes Ingress?
---
## Core Expertise Areas
- **Server blocks**: virtual hosts, default server, listen directives
- **Reverse proxy**: proxy_pass, proxy headers, WebSocket proxying
- **Load balancing**: round-robin, least_conn, ip_hash, weighted, health checks
- **SSL/TLS**: certificate config, HSTS, OCSP stapling, modern ciphers
- **Rate limiting**: limit_req_zone, limit_conn_zone, burst, nodelay
- **Caching**: proxy_cache, cache zones, cache bypass, stale cache
- **Security headers**: CSP, HSTS, X-Frame-Options, X-Content-Type-Options
- **Performance**: gzip, brotli, keepalive, worker tuning, open file cache
---
## Key Patterns & Code
### nginx.conf — Global Settings
```nginx
# /etc/nginx/nginx.conf
user www-data;
worker_processes auto; # one worker per CPU core
worker_rlimit_nofile 65535; # max open files per worker
pid /run/nginx.pid;
events {
worker_connections 4096; # max connections per worker
multi_accept on; # accept multiple connections at once
use epoll; # Linux epoll event model (most efficient)
}
http {
# ── Basic Settings ────────────────────────────────────────────────────
sendfile on; # efficient file sending
tcp_nopush on; # send headers in one packet
tcp_nodelay on; # disable Nagle for real-time
keepalive_timeout 65;
keepalive_requests 1000;
types_hash_max_size 2048;
server_tokens off; # hide Nginx version
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" '
'rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time';
access_log /var/log/nginx/access.log main buffer=16k flush=5s;
error_log /var/log/nginx/error.log warn;
# ── Gzip Compression ──────────────────────────────────────────────────
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
text/plain text/css text/xml text/javascript
application/json application/javascript application/xml
application/rss+xml application/atom+xml
image/svg+xml font/truetype font/opentype;
# ── Rate Limiting Zones ───────────────────────────────────────────────
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
# ── Proxy 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;
# ── Include Virtual Hosts ─────────────────────────────────────────────
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
```
### Reverse Proxy — Node.js / API
```nginx
# /etc/nginx/conf.d/api.conf
upstream api_backend {
least_conn; # send to least busy server
server 127.0.0.1:3000 weight=3;
server 127.0.0.1:3001 weight=1;
server 127.0.0.1:3002 backup; # only used if others are down
keepalive 32; # keep connections to upstream alive
}
server {
listen 80;
server_name api.example.com;
# Redirect HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
# ── SSL ───────────────────────────────────────────────────────────────
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/api.example.com/chain.pem;
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_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
# ── Security Headers ──────────────────────────────────────────────────
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
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 Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
# ── Request Limits ────────────────────────────────────────────────────
client_max_body_size 10m;
client_body_timeout 30s;
client_header_timeout 30s;
# ── Rate Limiting ─────────────────────────────────────────────────────
limit_req zone=api burst=20 nodelay;
limit_conn conn 20;
# ── Proxy Settings ────────────────────────────────────────────────────
location / {
proxy_pass http://api_backend;
proxy_http_version 1.1;
# Required headers
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 X-Request-ID $request_id;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Timeouts
proxy_connect_timeout 10s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
# Buffering
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
# Pass errors to client
proxy_intercept_errors off;
}
# ── Health check endpoint — no rate limit, no logging ─────────────────
location /health {
proxy_pass http://api_backend;
access_log off;
limit_req off;
}
# ── Block common attack paths ──────────────────────────────────────────
location ~* \.(?:git|env|htaccess|htpasswd)$ {
deny all;
return 404;
}
}
# WebSocket upgrade map
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
```
### Static Site with SPA Support
```nginx
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
root /var/www/app/dist;
index index.html;
# ── Aggressive caching for hashed assets ──────────────────────────────
location ~* \.(?:js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico|webp|avif)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# ── No cache for HTML — always fresh ──────────────────────────────────
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# ── SPA routing — serve index.html for all routes ─────────────────────
location / {
try_files $uri $uri/ /index.html;
}
# ── API proxy ─────────────────────────────────────────────────────────
location /api/ {
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;
proxy_http_version 1.1;
}
}
```
### Rate Limiting — Login Endpoint
```nginx
# Strict rate limiting for auth endpoints
location /api/auth/login {
limit_req zone=login burst=5 nodelay;
limit_req_status 429;
proxy_pass http://api_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;
# Custom 429 response
error_page 429 @rate_limited;
}
location @rate_limited {
default_type application/json;
add_header Retry-After 60 always;
return 429 '{"error":"Too many requests","retryAfter":60}';
}
```
### Proxy Caching
```nginx
location /api/products {
proxy_pass http://api_backend;
proxy_cache api_cache;
proxy_cache_key "$scheme$host$request_uri$http_authorization";
proxy_cache_valid 200 5m; # cache 200 responses for 5 minutes
proxy_cache_valid 404 1m; # cache 404 for 1 minute
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
proxy_cache_lock on; # prevent cache stampede
add_header X-Cache-Status $upstream_cache_status;
# Bypass cache for authenticated requests
proxy_cache_bypass $http_authorization;
proxy_no_cache $http_authorization;
}
```
### Load Balancing Strategies
```nginx
# Round-robin (default)
upstream backend_round_robin {
server 10.0.0.1:3000;
server 10.0.0.2:3000;
server 10.0.0.3:3000;
}
# Least connections — best for long-lived connections
upstream backend_least_conn {
least_conn;
server 10.0.0.1:3000;
server 10.0.0.2:3000;
server 10.0.0.3:3000;
}
# IP hash — sticky sessions (same client → same server)
upstream backend_sticky {
ip_hash;
server 10.0.0.1:3000;
server 10.0.0.2:3000;
}
# Weighted — send more traffic to powerful servers
upstream backend_weighted {
server 10.0.0.1:3000 weight=5; # gets 5/7 of traffic
server 10.0.0.2:3000 weight=2; # gets 2/7 of traffic
}
# With health checks (Nginx Plus feature, or use upstream_check module)
upstream backend_health {
server 10.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.2:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.3:3000 backup;
}
```
### Let's Encrypt with Certbot
```bash
# Install certbot
apt install certbot python3-certbot-nginx
# Get certificate (Nginx plugin — auto-configures nginx)
certbot --nginx -d example.com -d www.example.com
# Standalone (when nginx is not running)
certbot certonly --standalone -d example.com
# Wildcard certificate (requires DNS challenge)
certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d "example.com"
# Test auto-renewal
certbot renew --dry-run
# Auto-renewal is handled by systemd timer or cron
# Check it exists:
systemctl status certbot.timer
cat /etc/cron.d/certbot
```
### Debugging Common Issues
```bash
# Test config syntax
nginx -t
nginx -T # test and print full config
# Reload without downtime
nginx -s reload
systemctl reload nginx
# Check error logs
tail -f /var/log/nginx/error.log
journalctl -u nginx -f
# Debug specific request
# Add to location block temporarily:
# add_header X-Debug-URI $uri;
# add_header X-Debug-Upstream $upstream_addr;
# Check what nginx is listening on
ss -tlnp | grep nginx
# Check open file count
cat /proc/$(pgrep nginx | head -1)/limits | grep "open files"
# Common error meanings:
# 502 Bad Gateway → upstream is down or not accepting connections
# 504 Gateway Timeout → upstream is too slow (increase proxy_read_timeout)
# 499 Client Closed → client disconnected before response
# 413 Too Large → increase client_max_body_size
# Check upstream connectivity
curl -v http://127.0.0.1:3000/health
# Check if port is listening
ss -tlnp | grep :3000
```
### Docker Nginx Config
```nginx
# nginx.conf for Docker — no daemon mode
daemon off;
worker_processes auto;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
server_tokens off;
upstream app {
server app:3000; # Docker service name as hostname
}
server {
listen 80;
location / {
proxy_pass http://app;
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;
}
location /health {
proxy_pass http://app;
access_log off;
}
}
}
```
---
## Best Practices
- Always run `nginx -t` before reloading to catch config errors
- Use `server_tokens off` to hide Nginx version from attackers
- Set `client_max_body_size` explicitly — default is 1MB which is too small for file uploads
- Use `proxy_set_header X-Forwarded-For` so app sees real client IP
- Always set timeouts — proxy_connect_timeout, proxy_send_timeout, proxy_read_timeout
- Use `keepalive` on upstream blocks for connection reuse
- Set `worker_processes auto` to use all CPU cores
- Use `access_log off` on health check and static asset endpoints
- Always redirect HTTP to HTTPS — never serve on port 80 in production
---
## Common Pitfalls
| Pitfall | Problem | Fix |
|---|---|---|
| No `nginx -t` before reload | Bad config takes down Nginx | Always test before reload |
| Wrong upstream address in Docker | Cannot connect to backend | Use service name not localhost |
| Missing X-Forwarded-For header | App sees Nginx IP not client IP | Add proxy_set_header X-Forwarded-For |
| No timeout settings | Requests hang forever | Always set proxy timeouts |
| client_max_body_size too small | 413 errors on file uploads | Increase to match your max upload |
| HTTP not redirecting to HTTPS | Mixed content or insecure traffic | Add 301 redirect from port 80 |
| No rate limiting | DDoS / brute force possible | Add limit_req_zone on public endpoints |
| Caching authenticated responses | User A sees User B data | Add proxy_cache_bypass on auth headers |
---
## Related Skills
- **linux-expert**: For Linux server setup and management
- **docker-expert**: For Nginx in containers
- **kubernetes-expert**: For Nginx Ingress Controller
- **appsec-expert**: For security headers and hardening
- **webperf-expert**: For caching and compression optimization
- **cicd-expert**: For automated Nginx config deployment
Attribution
Comments
Loading comments…