Claude Code for NGINX and Caddy: Reverse Proxy, SSL, and Performance Tuning — Claude Skills 360 Blog
Blog / DevOps / Claude Code for NGINX and Caddy: Reverse Proxy, SSL, and Performance Tuning
DevOps

Claude Code for NGINX and Caddy: Reverse Proxy, SSL, and Performance Tuning

Published: December 3, 2026
Read time: 9 min read
By: Claude Skills 360

NGINX and Caddy serve different use cases: NGINX offers maximum performance and fine-grained control for high-traffic production systems; Caddy provides automatic HTTPS with zero SSL configuration and human-readable syntax for smaller deployments. Both handle reverse proxying, load balancing, caching, and security headers. Claude Code generates production-ready NGINX and Caddy configurations with proper SSL settings, security headers, rate limiting, WebSocket support, and the performance tuning that matters at scale.

CLAUDE.md for Web Server Configuration

## Web Server Context
- NGINX 1.26+ (stable) for high-traffic production (>1000 req/s)
- Caddy 2.x for smaller deployments needing automatic HTTPS
- SSL: TLS 1.2 min, TLS 1.3 preferred, HSTS preload
- Security headers: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- Rate limiting: per-IP with burst, separate limits for API vs static
- Caching: immutable static assets, no-store for API, short TTL for HTML
- Logging: JSON format for log aggregation (Datadog, Loki)
- Monitoring: NGINX stub_status or Caddy metrics endpoint → Prometheus

Production NGINX Configuration

# /etc/nginx/nginx.conf — global tuning
user nginx;
worker_processes auto;
worker_rlimit_nofile 65536;
error_log /var/log/nginx/error.log warn;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    # Performance
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    keepalive_requests 1000;
    
    # Compression
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 1000;
    gzip_types text/plain text/css application/json application/javascript
               text/xml application/xml application/xml+rss text/javascript
               image/svg+xml application/wasm;
    
    # Security
    server_tokens off;  # Don't expose NGINX version
    
    # JSON access log for aggregation
    log_format json_combined escape=json
        '{'
            '"time":"$time_iso8601",'
            '"remote_addr":"$remote_addr",'
            '"request":"$request",'
            '"status":$status,'
            '"bytes":$body_bytes_sent,'
            '"referrer":"$http_referer",'
            '"user_agent":"$http_user_agent",'
            '"request_time":$request_time,'
            '"upstream_time":"$upstream_response_time",'
            '"cache_status":"$upstream_cache_status"'
        '}';
    access_log /var/log/nginx/access.log json_combined;
    
    # Rate limiting zones (defined at http level, used in server blocks)
    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
    limit_req_zone $binary_remote_addr zone=general:10m rate=100r/m;
    
    # Connection limiting
    limit_conn_zone $binary_remote_addr zone=per_ip:10m;
    
    include /etc/nginx/conf.d/*.conf;
}
# /etc/nginx/conf.d/myapp.conf
upstream app_backend {
    least_conn;  # Send to least-busy backend
    
    server app1:3000 weight=1 max_fails=3 fail_timeout=30s;
    server app2:3000 weight=1 max_fails=3 fail_timeout=30s;
    server app3:3000 weight=1 max_fails=3 fail_timeout=30s;
    
    keepalive 32;  # Persistent connections to backends
}

# HTTP → HTTPS redirect
server {
    listen 80;
    server_name myapp.com www.myapp.com;
    return 301 https://myapp.com$request_uri;
}

# Main HTTPS server
server {
    listen 443 ssl;
    http2 on;
    server_name myapp.com;
    
    # SSL
    ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling on;
    ssl_stapling_verify on;
    
    # 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 Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.myapp.com; frame-ancestors 'none'" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    
    # Connection limits
    limit_conn per_ip 20;
    
    # Static assets — long cache, immutable
    location ~* \.(js|css|woff2|woff|ttf|svg|png|jpg|webp|avif)$ {
        root /var/www/myapp;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
    
    # API — rate limited, no caching
    location /api/ {
        limit_req zone=api burst=10 nodelay;
        limit_req_status 429;
        
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";  # Enable keepalive to 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;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        add_header Cache-Control "no-store" always;
        
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
    
    # WebSocket upgrade
    location /ws/ {
        proxy_pass http://app_backend;
        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_read_timeout 86400s;  # Keep WS connections alive for 24h
    }
    
    # Login endpoint — strict rate limit
    location /api/auth/login {
        limit_req zone=login burst=3 nodelay;
        limit_req_status 429;
        
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    # HTML pages — short cache
    location / {
        root /var/www/myapp;
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "public, max-age=300";
    }
}

Caddy Configuration

# Caddyfile — automatic HTTPS, human-readable syntax

{
    email [email protected]
    
    # Enable Prometheus metrics
    servers {
        metrics
    }
}

myapp.com {
    # Automatic TLS (Let's Encrypt) — no configuration needed
    
    # Security headers
    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server  # Remove Server header
    }
    
    # Rate limiting (caddy-ratelimit plugin)
    rate_limit {
        zone api {
            match {
                path /api/*
            }
            key {remote_host}
            events 30
            window 1m
        }
        zone login {
            match {
                path /api/auth/login
            }
            key {remote_host}
            events 5
            window 1m
        }
    }
    
    # Static assets with long cache
    @static {
        path *.js *.css *.woff2 *.png *.jpg *.webp *.svg
    }
    handle @static {
        root * /var/www/myapp
        file_server
        header Cache-Control "public, max-age=31536000, immutable"
    }
    
    # WebSocket
    @ws {
        header Connection *Upgrade*
        header Upgrade websocket
    }
    handle @ws {
        reverse_proxy localhost:3000 {
            transport http {
                dial_timeout 5s
            }
        }
    }
    
    # API reverse proxy with load balancing
    handle /api/* {
        reverse_proxy app1:3000 app2:3000 app3:3000 {
            lb_policy least_conn
            health_uri /health
            health_interval 10s
            health_timeout 5s
            fail_duration 30s
            
            header_up X-Real-IP {remote_host}
            header_up X-Forwarded-For {remote_host}
        }
        
        header Cache-Control "no-store"
    }
    
    # SPA fallback
    handle {
        root * /var/www/myapp
        try_files {path} /index.html
        file_server
        header Cache-Control "public, max-age=300"
    }
    
    log {
        output file /var/log/caddy/access.log
        format json
    }
}

For the Docker Compose setup that runs NGINX in front of your application containers, see the Docker guide for multi-container networking. For the Kubernetes Ingress controller that replaces NGINX at cluster level, the Kubernetes operators guide covers cluster-level traffic management. The Claude Skills 360 bundle includes NGINX and Caddy skill sets covering SSL configuration, rate limiting, and WebSocket proxying. Start with the free tier to try web server configuration generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free