nginx-config-pro
Overview
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+.
When to use
- Standing up nginx in front of an app server (Node, Django/gunicorn, Rails/puma, PHP-FPM, a container).
- Adding or fixing TLS, redirects, or HSTS.
- Diagnosing 502/504, header duplication, or caching misses.
- Reviewing an existing
nginx.conf for security and performance.
Workflow
Follow these steps in order. Each builds on the previous.
- Gather inputs. Determine: domain name(s), upstream address (host:port or socket), app protocol (http/https/grpc/websocket), whether TLS certs exist or need Let's Encrypt, expected traffic profile (for rate limits and cache), and static asset paths.
- Choose a topology. Single upstream vs. load-balanced pool; HTTP/1.1 keepalive to upstream vs. fresh connections; whether caching is safe (idempotent GET responses with sane cache headers).
- Scaffold the file. Use
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).
- Configure TLS. Apply the cipher suite, protocols, and OCSP stapling from
references/tls-hardening.md. Always serve a port 80 → 443 redirect. Add HSTS only once you are certain HTTPS is permanent.
- Set proxy headers correctly. Forward
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.
- Add performance layers. Enable gzip (and brotli if the module is present), HTTP/2, upstream keepalive, and
proxy_cache where appropriate.
- Add protection layers. Define
limit_req_zone / limit_conn_zone in http {} and apply them in location/server. Add the security-header bundle.
- Validate. Run
scripts/nginx_check.sh <conf> to lint for the most common mistakes, then nginx -t and reload. Verify externally per references/troubleshooting.md.
Proxy headers checklist (the #1 source of bugs)
Put shared proxy settings in a snippet (e.g. /etc/nginx/snippets/proxy.conf) and include it:
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 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:
- Use
$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.
- Set
Connection "" only when paired with an upstream { keepalive N; } block and proxy_http_version 1.1.
- Your app must trust these headers ONLY when nginx is the edge. Behind a load balancer/CDN, validate the real client IP via
set_real_ip_from + real_ip_header.
Worked decision: should I cache this?
| 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.
Rate limiting quick reference
Define zones in http {}, apply in server/location:
# 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.
- For login endpoints use a tighter zone (e.g.
rate=5r/m) to slow credential stuffing.
- Behind a CDN, key on the real client IP (after
real_ip), not the CDN edge IP.
Security headers bundle
Apply at server scope (see references/tls-hardening.md for full rationale and CSP guidance):
add_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.
Best Practices
- One server block per concern. Keep a dedicated port-80 redirect block separate from the TLS block.
- Always
server_tokens off; to hide the nginx version.
- Use snippets/includes for
proxy.conf, ssl.conf, and security-headers.conf so every site stays consistent.
- Pin
ssl_protocols TLSv1.2 TLSv1.3; and a modern cipher list; disable TLS 1.0/1.1.
- Test before reload:
nginx -t && nginx -s reload. Never reload a config that fails the test.
- Set explicit timeouts. Defaults (60s read) can hang clients on a slow upstream.
- Define a default
server_name _; block that returns 444 to drop requests for unknown hosts.
- Log to structured/JSON format when shipping to a log pipeline (see template).
- Reload, don't restart, for config changes — reload is zero-downtime.
Common Pitfalls
- Missing
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.
- HSTS too early /
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.
- Caching authenticated responses → leaks one user's data to another. Add
proxy_cache_bypass/proxy_no_cache for cookies/Authorization.
- Worker/file-descriptor limits → high-traffic sites need
worker_connections and worker_rlimit_nofile raised together.
Bundled files
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.
1---2name: nginx-config-pro3description: 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.4license: MIT5---67# nginx-config-pro89## Overview1011Authoritative 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.1213**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.1415This skill targets nginx 1.18+ (most directives work on 1.10+). HTTP/3 notes assume 1.25+.1617## When to use1819- Standing up nginx in front of an app server (Node, Django/gunicorn, Rails/puma, PHP-FPM, a container).20- Adding or fixing TLS, redirects, or HSTS.21- Diagnosing 502/504, header duplication, or caching misses.22- Reviewing an existing `nginx.conf` for security and performance.2324## Workflow2526Follow these steps in order. Each builds on the previous.27281. **Gather inputs.** Determine: domain name(s), upstream address (host:port or socket), app protocol (http/https/grpc/websocket), whether TLS certs exist or need Let's Encrypt, expected traffic profile (for rate limits and cache), and static asset paths.292. **Choose a topology.** Single upstream vs. load-balanced pool; HTTP/1.1 keepalive to upstream vs. fresh connections; whether caching is safe (idempotent GET responses with sane cache headers).303. **Scaffold the file.** Use `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).314. **Configure TLS.** Apply the cipher suite, protocols, and OCSP stapling from `references/tls-hardening.md`. Always serve a port 80 → 443 redirect. Add HSTS only once you are certain HTTPS is permanent.325. **Set proxy headers correctly.** Forward `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.336. **Add performance layers.** Enable gzip (and brotli if the module is present), HTTP/2, upstream keepalive, and `proxy_cache` where appropriate.347. **Add protection layers.** Define `limit_req_zone` / `limit_conn_zone` in `http {}` and apply them in `location`/`server`. Add the security-header bundle.358. **Validate.** Run `scripts/nginx_check.sh <conf>` to lint for the most common mistakes, then `nginx -t` and reload. Verify externally per `references/troubleshooting.md`.3637## Proxy headers checklist (the #1 source of bugs)3839Put shared proxy settings in a snippet (e.g. `/etc/nginx/snippets/proxy.conf`) and `include` it:4041```nginx42proxy_http_version 1.1;43proxy_set_header Host $host;44proxy_set_header X-Real-IP $remote_addr;45proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;46proxy_set_header X-Forwarded-Proto $scheme;47proxy_set_header X-Forwarded-Host $host;48proxy_set_header Connection ""; # enables upstream keepalive49proxy_read_timeout 60s;50proxy_connect_timeout 5s;51proxy_send_timeout 60s;52proxy_buffering on;53```5455Rules:56- Use `$host` (not `$http_host`) so a missing Host header degrades gracefully to `server_name`.57- `$proxy_add_x_forwarded_for` appends; never hard-set `X-Forwarded-For $remote_addr` behind another proxy.58- Set `Connection ""` only when paired with an `upstream { keepalive N; }` block and `proxy_http_version 1.1`.59- Your app must trust these headers ONLY when nginx is the edge. Behind a load balancer/CDN, validate the real client IP via `set_real_ip_from` + `real_ip_header`.6061## Worked decision: should I cache this?6263| Condition | Cache? |64|-----------|--------|65| Response is `GET`/`HEAD`, no `Set-Cookie`, no `Authorization` | Yes |66| Upstream sends `Cache-Control: private/no-store` | No (respect it) |67| Per-user/personalized HTML | No, or vary by a cache key that includes the session |68| Static assets (js/css/img with hashed names) | Yes, long `expires`, `immutable` |69| API JSON that changes every request | No |7071When caching, always add a `X-Cache-Status $upstream_cache_status` header so you can confirm HIT/MISS/BYPASS. See `references/caching-and-compression.md`.7273## Rate limiting quick reference7475Define zones in `http {}`, apply in `server`/`location`:7677```nginx78# http {} context79limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s;80limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;8182# location {} context83limit_req zone=req_per_ip burst=20 nodelay;84limit_conn conn_per_ip 20;85limit_req_status 429;86```8788- `burst` absorbs short spikes; `nodelay` serves the burst immediately instead of queuing.89- `$binary_remote_addr` is compact (16 bytes/IPv6); a 10m zone holds ~160k unique IPs.90- For login endpoints use a tighter zone (e.g. `rate=5r/m`) to slow credential stuffing.91- Behind a CDN, key on the real client IP (after `real_ip`), not the CDN edge IP.9293## Security headers bundle9495Apply at `server` scope (see `references/tls-hardening.md` for full rationale and CSP guidance):9697```nginx98add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;99add_header X-Content-Type-Options "nosniff" always;100add_header X-Frame-Options "SAMEORIGIN" always;101add_header Referrer-Policy "strict-origin-when-cross-origin" always;102add_header Content-Security-Policy "default-src 'self'" always;103server_tokens off;104```105106The `always` flag is mandatory — without it headers are dropped on 4xx/5xx responses.107108## Best Practices109110- **One server block per concern.** Keep a dedicated port-80 redirect block separate from the TLS block.111- **Always `server_tokens off;`** to hide the nginx version.112- **Use snippets/includes** for `proxy.conf`, `ssl.conf`, and `security-headers.conf` so every site stays consistent.113- **Pin `ssl_protocols TLSv1.2 TLSv1.3;`** and a modern cipher list; disable TLS 1.0/1.1.114- **Test before reload:** `nginx -t && nginx -s reload`. Never reload a config that fails the test.115- **Set explicit timeouts.** Defaults (60s read) can hang clients on a slow upstream.116- **Define a default `server_name _;` block** that returns 444 to drop requests for unknown hosts.117- **Log to structured/JSON format** when shipping to a log pipeline (see template).118- **Reload, don't restart**, for config changes — reload is zero-downtime.119120## Common Pitfalls121122- **Missing `Connection ""` + keepalive mismatch** → upstream connections not reused, or `502` storms. Pair `proxy_http_version 1.1`, `Connection ""`, and `upstream { keepalive }`.123- **`add_header` without `always`** → security headers silently disappear on errors.124- **`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.125- **`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.126- **HSTS too early / `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.127- **`502 Bad Gateway`** → upstream down, wrong port, or SELinux blocking the connection (`setsebool -P httpd_can_network_connect 1`). See `references/troubleshooting.md`.128- **`504 Gateway Timeout`** → slow upstream; raise `proxy_read_timeout` and fix the app, don't just mask it.129- **`413 Request Entity Too Large`** → raise `client_max_body_size` (default 1m) for uploads.130- **Caching authenticated responses** → leaks one user's data to another. Add `proxy_cache_bypass`/`proxy_no_cache` for cookies/Authorization.131- **Worker/file-descriptor limits** → high-traffic sites need `worker_connections` and `worker_rlimit_nofile` raised together.132133## Bundled files134135- `templates/reverse-proxy.conf.tmpl` — complete, fill-in server config with TLS, caching, rate limiting, gzip, websockets, and security headers.136- `references/tls-hardening.md` — cipher suites, protocols, OCSP stapling, HSTS, CSP, Let's Encrypt/certbot, real-IP behind CDN.137- `references/caching-and-compression.md` — proxy_cache zones, cache keys, bypass rules, gzip/brotli tuning, static asset strategy.138- `references/troubleshooting.md` — 502/504/413/444 diagnosis, header debugging, validation commands.139- `scripts/nginx_check.sh` — stdlib (bash/grep) linter for the most common nginx config mistakes.140- `examples/node-app-proxy.md` — worked example: proxying a Node app on :3000 to https://app.example.com.