Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Generates and hardens production nginx configurations for reverse proxying with TLS termination, HTTP/2, response caching, rate limiting, gzip/brotli compression, and security headers. Use this skill when the user asks to "set up nginx as a reverse proxy", "add TLS/SSL to nginx", "configure nginx caching or rate limiting", "harden an nginx config", "proxy an app behind nginx", "fix nginx 502/504 errors", or write/review nginx.conf and server blocks.
.claude/skills/jayrha-nginx-config-pro/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 43% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 92% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 171% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 79% | 0% |
Authoritative guidance for writing secure, performant nginx reverse-proxy configurations. Produces server blocks that terminate TLS, speak HTTP/2, proxy to upstream applications, cache responses, throttle abusive clients, and compress payloads — without the footguns that cause 502s, header leaks, or weak ciphers.
Keywords: nginx, reverse proxy, TLS, SSL, HTTPS, HTTP/2, HTTP/3, certbot, Let's Encrypt, upstream, proxy_pass, gzip, brotli, rate limiting, limit_req, proxy_cache, security headers, HSTS, CSP, OCSP stapling, websocket, load balancing, 502 bad gateway, 504 gateway timeout.
This skill targets nginx 1.18+ (most directives work on 1.10+). HTTP/3 notes assume 1.25+.
nginx.conf for security and performance.Follow these steps in order. Each builds on the previous.
templates/reverse-proxy.conf.tmpl as the starting point. Place site configs in /etc/nginx/sites-available/<name>.conf and symlink into sites-enabled/ (Debian/Ubuntu), or /etc/nginx/conf.d/<name>.conf (RHEL).references/tls-hardening.md. Always serve a port 80 → 443 redirect. Add HSTS only once you are certain HTTPS is permanent.Host, X-Forwarded-For, X-Forwarded-Proto, and X-Real-IP. Set sane timeouts. For websockets, add the Upgrade/Connection map. See the "Proxy headers" checklist below.proxy_cache where appropriate.limit_req_zone / limit_conn_zone in http {} and apply them in location/server. Add the security-header bundle.scripts/nginx_check.sh <conf> to lint for the most common mistakes, then nginx -t and reload. Verify externally per references/troubleshooting.md.Put shared proxy settings in a snippet (e.g. /etc/nginx/snippets/proxy.conf) and include it:
nginxproxy_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 X-Forwarded-Host $host; proxy_set_header Connection ""; # enables upstream keepalive proxy_read_timeout 60s; proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_buffering on;
Rules:
$host (not $http_host) so a missing Host header degrades gracefully to server_name.$proxy_add_x_forwarded_for appends; never hard-set X-Forwarded-For $remote_addr behind another proxy.Connection "" only when paired with an upstream { keepalive N; } block and proxy_http_version 1.1.set_real_ip_from + real_ip_header.| Condition | Cache? | |-----------|--------| | Response is GET/HEAD, no Set-Cookie, no Authorization | Yes | | Upstream sends Cache-Control: private/no-store | No (respect it) | | Per-user/personalized HTML | No, or vary by a cache key that includes the session | | Static assets (js/css/img with hashed names) | Yes, long expires, immutable | | API JSON that changes every request | No |
When caching, always add a X-Cache-Status $upstream_cache_status header so you can confirm HIT/MISS/BYPASS. See references/caching-and-compression.md.
Define zones in http {}, apply in server/location:
nginx# http {} context limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s; limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m; # location {} context limit_req zone=req_per_ip burst=20 nodelay; limit_conn conn_per_ip 20; limit_req_status 429;
burst absorbs short spikes; nodelay serves the burst immediately instead of queuing.$binary_remote_addr is compact (16 bytes/IPv6); a 10m zone holds ~160k unique IPs.rate=5r/m) to slow credential stuffing.real_ip), not the CDN edge IP.Apply at server scope (see references/tls-hardening.md for full rationale and CSP guidance):
nginxadd_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'" always; server_tokens off;
The always flag is mandatory — without it headers are dropped on 4xx/5xx responses.
server_tokens off; to hide the nginx version.proxy.conf, ssl.conf, and security-headers.conf so every site stays consistent.ssl_protocols TLSv1.2 TLSv1.3; and a modern cipher list; disable TLS 1.0/1.1.nginx -t && nginx -s reload. Never reload a config that fails the test.server_name _; block that returns 444 to drop requests for unknown hosts.Connection "" + keepalive mismatch → upstream connections not reused, or 502 storms. Pair proxy_http_version 1.1, Connection "", and upstream { keepalive }.add_header without always → security headers silently disappear on errors.add_header inheritance trap → defining ANY add_header in a location discards ALL add_header from the parent server. Re-include the full bundle or use a snippet in every location that needs it.proxy_pass trailing-slash semantics → proxy_pass http://up/; (with slash) strips the matched location prefix; without the slash it passes the full URI. Mixing these breaks routing.preload before submission → can lock users out of HTTP for max-age. Start with a small max-age, drop preload until you have submitted to hstspreload.org.502 Bad Gateway → upstream down, wrong port, or SELinux blocking the connection (setsebool -P httpd_can_network_connect 1). See references/troubleshooting.md.504 Gateway Timeout → slow upstream; raise proxy_read_timeout and fix the app, don't just mask it.413 Request Entity Too Large → raise client_max_body_size (default 1m) for uploads.proxy_cache_bypass/proxy_no_cache for cookies/Authorization.worker_connections and worker_rlimit_nofile raised together.templates/reverse-proxy.conf.tmpl — complete, fill-in server config with TLS, caching, rate limiting, gzip, websockets, and security headers.references/tls-hardening.md — cipher suites, protocols, OCSP stapling, HSTS, CSP, Let's Encrypt/certbot, real-IP behind CDN.references/caching-and-compression.md — proxy_cache zones, cache keys, bypass rules, gzip/brotli tuning, static asset strategy.references/troubleshooting.md — 502/504/413/444 diagnosis, header debugging, validation commands.scripts/nginx_check.sh — stdlib (bash/grep) linter for the most common nginx config mistakes.examples/node-app-proxy.md — worked example: proxying a Node app on :3000 to https://app.example.com.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 15,734 | 12,184 | -23% | 1 | 1 | 0% | 3,713 | 5,304 | +43% | 0 | 0 | — |
case-02 | fail→pass | 19,340 | 14,377 | -26% | 1 | 1 | 0% | 3,980 | 5,500 | +38% | 0 | 0 | — |
case-03 | pass→pass | 14,761 | 15,302 | +4% | 1 | 1 | 0% | 2,855 | 5,479 | +92% | 0 | 0 | — |
case-04 | pass→pass | 7,136 | 6,043 | -15% | 1 | 1 | 0% | 1,369 | 3,714 | +171% | 0 | 0 | — |
case-05 | pass→pass | 12,487 | 10,949 | -12% | 1 | 1 | 0% | 2,614 | 4,668 | +79% | 0 | 0 | — |
case-06 | pass→pass | 11,674 | 6,229 | -47% | 1 | 1 | 0% | 2,304 | 3,717 | +61% | 0 | 0 | — |
case-07 | pass→pass | 7,536 | 6,089 | -19% | 1 | 1 | 0% | 1,466 | 3,621 | +147% | 0 | 0 | — |
case-08 | pass→pass | 11,232 | 7,099 | -37% | 1 | 1 | 0% | 2,204 | 3,964 | +80% | 0 | 0 | — |
case-09 | pass→pass | 8,657 | 5,805 | -33% | 1 | 1 | 0% | 1,785 | 3,734 | +109% | 0 | 0 | — |
case-10 | pass→pass | 12,701 | 6,806 | -46% | 1 | 1 | 0% | 2,306 | 3,907 | +69% | 0 | 0 | — |
case-11 | pass→pass | 10,559 | 7,749 | -27% | 1 | 1 | 0% | 2,150 | 4,054 | +89% | 0 | 0 | — |
case-12 | pass→pass | 3,880 | 3,806 | -2% | 1 | 1 | 0% | 694 | 3,217 | +364% | 0 | 0 | — |
case-13 | pass→pass | 4,159 | 3,545 | -15% | 1 | 1 | 0% | 870 | 3,239 | +272% | 0 | 0 | — |
case-14 | pass→pass | 4,889 | 2,810 | -43% | 1 | 1 | 0% | 732 | 2,903 | +297% | 0 | 0 | — |
case-15 | pass→pass | 15,298 | 12,219 | -20% | 1 | 1 | 0% | 2,998 | 5,012 | +67% | 0 | 0 | — |
case-21 | pass→pass | 9,173 | 3,632 | -60% | 1 | 1 | 0% | 1,738 | 3,145 | +81% | 0 | 0 | — |
case-16 | pass→pass | 8,789 | 5,888 | -33% | 1 | 1 | 0% | 1,769 | 3,645 | +106% | 0 | 0 | — |
case-17 | pass→pass | 9,420 | 7,676 | -19% | 1 | 1 | 0% | 1,907 | 3,976 | +108% | 0 | 0 | — |
case-18 | pass→pass | 11,835 | 19,004 | +61% | 1 | 1 | 0% | 2,143 | 4,114 | +92% | 0 | 0 | — |
case-19 | pass→pass | 6,331 | 5,332 | -16% | 1 | 1 | 0% | 1,268 | 3,531 | +178% | 0 | 0 | — |
case-20 | pass→pass | 11,851 | 8,168 | -31% | 1 | 1 | 0% | 2,566 | 4,180 | +63% | 0 | 0 | — |
case-22 | pass→pass | 5,445 | 3,699 | -32% | 1 | 1 | 0% | 1,054 | 3,230 | +206% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/3/2026 | +9% |
Other measured skills in the registry, with their headline benchmark lift.