Configures and debugs nginx: reverse proxy, load balancing, SSL/TLS termination, caching, redirects, and static file serving. Use when writing or reviewing nginx.conf, server blocks, locations, upstreams, or proxy_pass, when a site behind nginx throws 502, 504, 413, 403, or a redirect loop, when WebSockets, SSE, or gRPC break through the proxy, when a certificate works in curl but warns in browsers, when requests hit the wrong location or the backend sees the wrong path, when tuning workers, buffers, gzip, or proxy cache, when rate limiting or blocking abuse, when proxying raw TCP/UDP, or when nginx runs in Docker or Kubernetes. Not for certificate issuance or renewal (ACME, Let's Encrypt) — that is the ssl skill.
---
name: Nginx
slug: nginx
version: 1.0.5
description: >-
Configures and debugs nginx: reverse proxy, load balancing, SSL/TLS termination, caching,
redirects, and static file serving. Use when writing or reviewing nginx.conf, server blocks,
locations, upstreams, or proxy_pass, when a site behind nginx throws 502, 504, 413, 403, or a
redirect loop, when WebSockets, SSE, or gRPC break through the proxy, when a certificate works
in curl but warns in browsers, when requests hit the wrong location or the backend sees the
wrong path, when tuning workers, buffers, gzip, or proxy cache, when rate limiting or blocking
abuse, when proxying raw TCP/UDP, or when nginx runs in Docker or Kubernetes. Not for
certificate issuance or renewal (ACME, Let's Encrypt) — that is the ssl skill.
homepage: https://clawic.com/skills/nginx
changelog: "Display name shown correctly"
metadata:
clawdbot:
emoji: 🌐
displayName: Nginx
configPaths:
- ~/Clawic/data/nginx/
---
User preferences and memory live in `~/Clawic/data/nginx/` (see `setup.md` on first use, `memory-template.md` for the file format). If you have data at an old location (`~/nginx/` or `~/clawic/nginx/`), move it to `~/Clawic/data/nginx/`.
## Configuration
User-dependent variables. Defaults apply until the user states a preference; store them in `~/Clawic/data/nginx/config.yaml`.
| Variable | Type | Default | Effect |
|---|---|---|---|
| os_family | debian \| rhel \| alpine | debian | Selects package/config layout (sites-enabled vs conf.d), makes SELinux the first 502/403 suspect on rhel (`debug.md`), and sets module install paths |
| deployment | systemd \| docker \| kubernetes | systemd | Selects reload commands and resolver address; docker/kubernetes routes advice through `containers.md` |
| edge_position | standalone \| behind-cdn-lb | standalone | behind-cdn-lb turns on realip guidance, switches redirect logic from `$scheme` to `X-Forwarded-Proto`, and flags rate-limit keying on the LB address |
Preference areas to record as the user reveals them:
- **tooling** — OSS vs Plus, mainline vs stable channel, dynamic modules in use (brotli, headers-more, njs); governs which directives are assumed available
- **conventions** — config layout (conf.d vs sites-enabled vs single file), snippet/include organization, upstream and zone naming; governs where examples place directives
- **safety posture** — 302-before-301 rollout ramp, HSTS ramp speed, confirm-before-reload on production hosts; governs pacing in `redirects.md` and `ssl.md`
- **platform** — IPv6 listeners, HTTP/2/3 adoption, which CDN/LB sits in front; governs listen directives and header-trust guidance
## When To Use
- Writing or reviewing nginx config: server blocks, locations, proxy_pass, upstreams
- Debugging 502/504/413/403/redirect loops behind an nginx reverse proxy
- SSL/TLS termination, HTTP/2, WebSocket, gRPC, or SSE through nginx
- Tuning: workers, buffers, gzip, proxy cache, rate limiting
- Proxying raw TCP/UDP (databases, TLS passthrough, syslog) and operating nginx (upgrades, monitoring, log rotation)
- Not for certificate issuance/renewal itself (ACME, Let's Encrypt) — nginx consumes certs; issuance is the `ssl` skill
## Quick Reference
| Situation | Play |
|-----------|------|
| Random 502 after a container/backend redeploy | DNS cached at startup — use variable in `proxy_pass` + `resolver` (`proxy.md`) |
| 502 immediately vs 504 after ~60s | 502 = refused/reset/bad response; 504 = timeout. Different fixes (`proxy.md`) |
| 413 on uploads | `client_max_body_size` — default is 1m; raise in `http` or the exact `server`/`location` |
| WebSocket connects then dies, or never upgrades | Upgrade trio + timeout (`proxy.md`) |
| SSE/streaming arrives all at once | `proxy_buffering off` for that location (`proxy.md`) |
| gRPC calls fail through nginx, direct works | `grpc_pass`, not `proxy_pass` — trailers and HTTP/2 (`proxy.md`) |
| Wrong file served / 403 on aliased path | `root` vs `alias` semantics and the alias-traversal slash bug (`semantics.md`) |
| Request hits wrong location block | Re-derive with the matching algorithm below; `nginx -T` to see effective config |
| Backend receives wrong path (`/api/api/...` or missing prefix) | proxy_pass trailing-slash rules (below) |
| Browser cert warning, curl works | Missing intermediates — serve fullchain (`ssl.md`) |
| 80→443 redirect loop behind a CDN/LB | Trust `X-Forwarded-Proto`, don't redirect on `$scheme` alone (`ssl.md`) |
| Redirect fixed in config but browser still loops | Cached 301 — test in curl or a private window (`redirects.md`) |
| PHP blank page, "File not found", or browser downloads .php source | SCRIPT_FILENAME and location order (`fastcgi.md`) |
| nginx container exits instantly, or template renders empty values | Foreground mode and the envsubst collision (`containers.md`) |
| Proxy a database / route TLS by SNI without terminating / forward syslog | `stream {}` block, `ssl_preread` (`stream.md`) |
| Upgrade the nginx binary or a module without dropping connections | USR2/WINCH signal sequence (`operations.md`) |
| "Is nginx overloaded?" / capacity monitoring | `stub_status` and what its numbers mean (`operations.md`) |
| Slow under load, high CPU or connection errors | `performance.md` |
| Security headers vanished on some routes | `add_header` inheritance trap (`semantics.md`) |
| Config behaves unlike it reads (`if`, variables, includes, root/alias) | `semantics.md` |
| Anything else | Debugging Order below, then the closest file above |
Depth on demand: `debug.md` startup failures, status-code decoder, tracing · `proxy.md` 502/504, DNS trap, WebSocket, gRPC, buffering, retries · `semantics.md` root/alias, inheritance, `if`, variables, includes, server selection · `redirects.md` return/rewrite, status codes, canonical host · `ssl.md` chain, baseline, HSTS, OCSP, mTLS, HTTP/3 · `performance.md` workers, buffers, gzip, proxy cache · `security.md` rate/conn limits, auth, hardening · `fastcgi.md` PHP-FPM · `containers.md` Docker/K8s · `stream.md` TCP/UDP, TLS passthrough · `operations.md` signals, upgrades, monitoring, log rotation.
## Core Rules
1. `nginx -t && nginx -s reload` — never restart to apply config; reload is graceful (old workers finish in-flight requests). Test first: a bad config on restart takes the site down; on reload it's rejected.
2. Read the effective config with `nginx -T`, not the files — includes, inheritance, and distro defaults (`/etc/nginx/conf.d/*`) mean the file you're editing may not be what runs.
3. One canonical `Host` line: `proxy_set_header Host $host;`. Without it the backend sees the upstream name from `proxy_pass` — breaks virtual hosts, redirects, and anything that reads Host.
4. `proxy_set_header` in a location wipes ALL inherited proxy headers from server/http level, same for `add_header`. Inheritance is all-or-nothing per level: if you set one header in a location, re-declare the full set there.
5. Sizing: max concurrent proxied clients ≈ `worker_processes × worker_connections / 2` (each proxied request holds a client fd and an upstream fd). 4 workers × 1024 connections → ~2048 clients. Set `worker_rlimit_nofile` ≥ 2× worker_connections.
6. Never put logic in `if` beyond `return`/`rewrite` — `if` in location context creates a pseudo-location where other directives misbehave. Use `map` for conditionals.
7. Diagnose from the log split: log `$request_time` and `$upstream_response_time` together. High request_time + low upstream_time = slow client or buffering problem; both high = slow backend. Without both numbers you're guessing which side is slow.
## Location Matching (the real algorithm)
Common misreading is "regex beats prefix". Actual order:
1. Exact `= /path` — match ends immediately.
2. Find the LONGEST matching prefix (order in file irrelevant for prefixes).
3. If that prefix is marked `^~` — use it, skip regex entirely.
4. Otherwise try regex locations `~` / `~*` in FILE ORDER — first regex match wins.
5. No regex matched — fall back to the longest prefix from step 2.
Consequences:
- `location /api` also matches `/api-v2`, `/apiary` — prefix is string prefix, not path segment. Use `location /api/` plus `location = /api` if you need the segment.
- A short regex declared early beats your long careful prefix — `^~` on static asset prefixes is the standard defense.
- `location /api/` does not match `/api` (no trailing slash) — pair with exact match or accept the 404.
## proxy_pass Path Rules
- `proxy_pass http://backend;` (no URI part) → request path passed unchanged: `/api/users` → `/api/users`.
- `proxy_pass http://backend/;` (any URI part, even just `/`) → matched location prefix is REPLACED by that URI: `location /api/` + `.../` → `/api/users` becomes `/users`.
- URI part inside a regex location or inside `if` = config error at startup — rewrite instead, or drop the URI part.
- With a variable in `proxy_pass` (`set $up http://backend; proxy_pass $up;`) path handling changes again: nginx passes the URI as given in the directive; combine with `$request_uri` explicitly if needed.
- Verify with the backend's access log or `curl -v` against the backend directly — not by reasoning about the config.
## Proxy Headers & Real IP
Canonical block (re-declare wholesale wherever any `proxy_set_header` appears — rule 4):
```nginx
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;
```
- `$host` = lowercase, no port; `$http_host` = raw header with port. APIs that generate absolute URLs usually need `$http_host`.
- `X-Forwarded-For` is client-spoofable. If nginx is behind a trusted LB/CDN, use the realip module: `set_real_ip_from <LB subnet>; real_ip_header X-Forwarded-For; real_ip_recursive on;` — otherwise rate limits and logs key on the LB's IP.
- Headers with underscores are silently dropped by default (`underscores_in_headers on` to keep) — a classic "auth works with curl -H, fails through nginx" cause.
## Upstream & Keepalive
```nginx
upstream backend {
server 10.0.0.2:3000 max_fails=3 fail_timeout=10s;
server 10.0.0.3:3000 max_fails=3 fail_timeout=10s;
keepalive 32;
}
```
- `keepalive 32` does NOTHING alone. The trio: `keepalive N` in upstream + `proxy_http_version 1.1;` + `proxy_set_header Connection "";` in the location. Missing either latter directive = a new TCP (and TLS) handshake per request, silently.
- `keepalive N` = idle connections kept per worker, not a connection limit.
- Defaults: `max_fails=1 fail_timeout=10s`. `fail_timeout` is dual-purpose: the window for counting failures AND the ban duration. `max_fails=0` disables marking down entirely.
- These are passive checks (real requests fail first). Active health checks are nginx Plus only — in OSS, put a real health endpoint behind your monitoring instead.
- Server without a port = port 80 — a common surprise when the app listens on 3000.
- Retries: on error/timeout nginx tries the next upstream server. Non-idempotent methods (POST, PATCH, LOCK) are not retried since nginx >=1.9.13 unless you set `proxy_next_upstream non_idempotent` — do not set it for endpoints with side effects.
## try_files & Static
- SPA: `try_files $uri $uri/ /index.html;` — file, then directory (needs `index`), then internal fallback. Last arg is a redirect/code, not a checked file: `=404` to error instead.
- `try_files` + `proxy_pass` in one location: try_files controls; route to the proxy via a named location — `try_files $uri @app;` + `location @app { proxy_pass ...; }`. This is the canonical "static if present, else app" pattern.
- Static asset locations: `^~` prefix, `access_log off;`, `expires 30d;` + `add_header Cache-Control "public, immutable";` for hashed filenames (expires alone without Cache-Control gets ignored by some clients).
- `sendfile on; tcp_nopush on;` together — sendfile without tcp_nopush leaves the kernel optimization on the table.
## SSL/TLS Essentials
- `ssl_certificate` takes the FULLCHAIN (leaf + intermediates, leaf first, no root). Leaf-only "works" in browsers with cached intermediates and fails on fresh clients — the classic "works for me, warning for users".
- Baseline: `ssl_protocols TLSv1.2 TLSv1.3;` and start from the Mozilla SSL config generator (intermediate profile) rather than hand-picking ciphers.
- `ssl_ciphers` only governs ≤TLS1.2; TLS1.3 suites need `ssl_conf_command Ciphersuites` (OpenSSL) — a source of "my cipher config does nothing".
- `ssl_prefer_server_ciphers off` is the modern recommendation (client-hardware-aware selection); `on` was TLS1.2-era advice.
- `ssl_session_cache shared:SSL:10m;` — ~4000 sessions per MB, shared across workers. Skipping it costs a full handshake per returning client.
- Redirect loops, HSTS rollout, OCSP, client certs → `ssl.md`.
## Rate Limiting
```nginx
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ { limit_req zone=api burst=20 nodelay; limit_req_status 429; }
```
- `rate=10r/s` is enforced per-millisecond: 1 request per 100ms. Two instant requests without `burst` = second one rejected. Always pair rate with a burst sized for legitimate client behavior (a browser page load fires 10-30 parallel requests).
- `nodelay` = serve the burst immediately, refill over time; without it, burst requests queue and add latency.
- Default rejection status is 503 — set 429 so clients and monitoring can tell throttling from outage.
- Behind a CDN/LB without realip configured, `$binary_remote_addr` is the LB's address — you rate-limit everyone as one client.
- Zone sizing, tiered keys, connection/bandwidth limits → `security.md`.
## Debugging Order
1. `nginx -t` — config valid? `nginx -T` — what's actually loaded?
2. `tail -f` the error log; raise level only on the suspect vhost. Grep-to-cause:
| Error log message | Meaning |
|---|---|
| `connect() failed (111: Connection refused)` | Backend down or wrong port → 502 |
| `no live upstreams` | All upstream servers marked failed (max_fails tripped) → 502 |
| `upstream prematurely closed connection` | Backend crashed mid-response or app timeout shorter than nginx's → 502 |
| `upstream timed out (110)` | nginx waited `proxy_read_timeout` (default 60s) → 504 |
| `client intended to send too large body` | `client_max_body_size` (default 1m) → 413 |
| `worker_connections are not enough` | Connection budget exhausted → see Core Rule 5 |
| Anything else | Reproduce with `curl -v` against nginx AND directly against the backend; the diff localizes the fault |
3. A timeout in nginx does not cancel the backend request — the backend keeps burning CPU on a request nobody will read. Fix the slow endpoint, don't just raise the timeout.
## Output Gates
Before emitting an nginx config or config advice, verify:
- Apply instructions end with `nginx -t` then reload — never a restart?
- Every location that sets any `proxy_set_header` or `add_header` re-declares the full inherited set (rule 4)?
- `proxy_pass` URI part checked against the trailing-slash rules — the backend receives the path you intend?
- Upload-handling routes have `client_max_body_size` above the real payload size?
- New permanent redirects shipped as 302 first, 301 only after verification (`redirects.md` ramp)?
- If `edge_position` is behind-cdn-lb: realip configured before anything keys on client IP, and redirects read `X-Forwarded-Proto`?
## Traps
| Trap | Why it fails | Do instead |
|---|---|---|
| `worker_processes auto` in containers | Reads host CPU count, not cgroup quota — 64 workers on a 2-CPU limit | Set explicitly to the container's CPU limit |
| Editing config, reloading, no change | Distro includes another file that wins | `nginx -T \| less`, find who owns the directive |
| Hostname in `proxy_pass` to dynamic infra | Resolved once at startup, cached forever | Variable + `resolver` (`proxy.md`) |
| `gzip on` for images/zip/woff2 | Recompressing compressed data: CPU spent, bytes gained | `gzip_types` with text formats only |
| `if` for routing logic | Pseudo-location; directives inside behave unpredictably | `map` + variable, or separate locations |
| Log rotation without signal | nginx keeps writing to the deleted inode; disk fills with no visible file | `nginx -s reopen` (USR1) in the rotate script (`operations.md`) |
| Reload "not taking" with WebSockets | Old workers stay alive until long-lived connections close | `worker_shutdown_timeout 30s;` to bound the drain |
| No `default_server` defined | First server block silently catches all unmatched Hosts | Explicit `listen 80 default_server; return 444;` catch-all (`semantics.md`) |
## Where Experts Disagree
- **Mainline vs stable channel.** The nginx team recommends mainline for most users; "stable" means fewer feature changes, not more reliability. Default: mainline from nginx.org repos when you control the host; pinned distro/stable inside images where reproducibility wins (`operations.md`).
- **conf.d vs sites-enabled.** Debian's symlink pattern adds an explicit enable/disable step; flat conf.d is simpler. Either works — the failure mode is mixing both and losing track of what's live; `nginx -T` is the referee (rule 2).
- **Terminate TLS at the CDN/LB or at nginx.** Edge termination centralizes cert management; nginx termination keeps encryption to the box. The boundary is compliance scope and who owns header trust — whoever terminates must set `X-Forwarded-Proto` and realip correctly (`ssl.md`).
## Related Skills
More Clawic skills, get them at https://clawic.com/skills/nginx (install if the user confirms):
- `ssl` — certificate issuance, renewal, and TLS debugging beyond nginx directives
- `docker` — nginx in containers: images, networks, and the resolver at 127.0.0.11
- `caddy` — when automatic HTTPS and a simpler config beat nginx's control
- `vps` — server provisioning and hardening around the nginx install
- `dns` — records and propagation issues upstream of the proxy
## Feedback
- If useful, star it: https://clawic.com/skills/nginx
- Latest version: https://clawic.com/skills/nginx
Part of [Clawic](https://clawic.com), the verified skill library. Get this skill: https://clawic.com/skills/nginx.
don't have the plugin yet? install it then click "run inline in claude" again.
nginx handles reverse proxying, load balancing, SSL/TLS termination, caching, redirects, and static file serving. use this skill when you're writing or reviewing nginx.conf, server blocks, locations, upstreams, or proxy_pass directives; debugging 502, 504, 413, 403, or redirect loops; routing WebSockets, SSE, or gRPC through the proxy; fixing certificate warnings; tuning workers, buffers, gzip, or proxy cache; implementing rate limiting; proxying raw TCP/UDP; or running nginx in Docker/Kubernetes. not for certificate issuance or renewal (ACME, Let's Encrypt) , those live in the ssl skill.
~/Clawic/data/nginx/ (see setup.md on first use, memory-template.md for the file format). if you have data at an old location (~/nginx/ or ~/clawic/nginx/), move it to ~/Clawic/data/nginx/.nginx.conf (location varies by distro: /etc/nginx/nginx.conf on Debian/RHEL, /etc/nginx/conf.d/*.conf drops, /etc/nginx/sites-enabled/* symlinks, or single file depending on layout)./etc/resolv.conf), but nginx stream block and resolver directives inside http context can override. in Docker/Kubernetes, provide the resolver IP (Kubernetes: 10.96.0.10 or the cluster DNS, Docker: 127.0.0.11).stub_status endpoint or Prometheus exporter integration for load inspection.store these in ~/Clawic/data/nginx/config.yaml and update as the user reveals them:
| preference | values | effect |
|---|---|---|
| os_family | debian / rhel / alpine | selects package/config layout, makes SELinux the first 502/403 suspect on rhel, and sets module install paths |
| deployment | systemd / docker / kubernetes | selects reload commands and resolver address; docker/kubernetes routes advice through containers.md |
| edge_position | standalone / behind-cdn-lb | behind-cdn-lb turns on realip guidance, switches redirect logic from $scheme to X-Forwarded-Proto, flags rate-limit keying on the LB address |
| tooling | oss / nginx-plus, mainline / stable, modules (brotli, headers-more, njs, etc.) | governs which directives are assumed available |
| config_layout | conf.d / sites-enabled / single-file | governs where examples place directives |
| snippet_organization | how includes and shared blocks are named | governs naming patterns in suggestions |
| upstream_naming | convention (e.g., backend_http, api_grpc, db_stream) |
governs naming consistency |
| safety_posture | 302-before-301, hsts-ramp-speed, confirm-before-reload | governs pacing in redirects.md and ssl.md |
| ipv6_listeners | yes / no | governs listen directives |
| http_version_adoption | http/1.1 only, http/2, http/3 | governs listen directives and keepalive configuration |
| cdn_lb_in_front | cdn name / load balancer type | governs header-trust guidance and realip subnet |
nginx -t to syntax-check without reloading.nginx -T to dump the effective configuration (all includes, inheritance, and distro defaults resolved)./var/log/nginx/error.log; distro-dependent).tail -f /var/log/nginx/error.log and reproduce the issue (browser request, curl, test client).error_log /var/log/nginx/error.log debug; in the http block for that server block, reload, and retry.proxy_pass or upstream directive.curl -v http://backend-host:port/path or nc -zv backend-host port (TCP); this bypasses nginx entirely.-v output./api/users from GET /api/users?page=1 HTTP/1.1).location blocks in the relevant server block.= beats all, longest prefix beats short prefix (file order irrelevant), ^~ prefix blocks regex, regex in file order, fall back to longest prefix.^~, or add an exact match.proxy_pass directive and current proxy_set_header statements in the matched location.Host, X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto are set correctly (see core rules section for canonical form).proxy_pass http://api.example.com;), verify DNS resolution:curl -v http://api.example.com:80 to test resolution and TCP connect from the nginx host.ping api.example.com works, nginx's resolver config may be stale; add resolver 8.8.8.8; in the location and reload.proxy_pass $backend_upstream;), ensure a resolver directive exists in the http block and the variable is set before use.proxy_pass directive, location prefix, and the request URI.proxy_pass has no URI part (e.g., proxy_pass http://backend;), the request path passes unchanged.proxy_pass has a URI part (e.g., proxy_pass http://backend/; or proxy_pass http://backend/api;), the matched location prefix is REPLACED by that URI.proxy_pass, path handling changes: nginx passes the URI as given in the directive; combine with $request_uri explicitly if you need the original path.curl -v against the backend directly.ssl_certificate, ssl_certificate_key, ssl_protocols, ssl_ciphers from the server block.ssl_certificate points to the FULLCHAIN file (leaf + intermediates, leaf first, no root certificate).openssl x509 -in /path/to/cert -text -noout | head -20 to inspect the certificate; count how many certificates are in the chain file with openssl crl2pkcs7 -nocrl -certfile /path/to/cert | openssl pkcs7 -print_certs -noout | grep "subject=" | wc -l.ssl_protocols includes TLSv1.2 and TLSv1.3 (minimum for 2024).ssl_session_cache shared:SSL:10m; is set (avoids full handshake on reconnect).ssl_conf_command Ciphersuites is present (TLSv1.3 ciphers do not use ssl_ciphers).curl -v https://example.com/path (or http if testing in dev).$request_time) and $upstream_response_time (also logged) to identify where latency lives: high request_time + low upstream_time points to slow client or buffering; both high points to slow backend.nginx -s reload (or systemctl reload nginx if systemd manages it).systemctl restart nginx on production; restart closes all connections; reload drains old workers gracefully.ps aux | grep nginx shows old and new worker processes briefly, then only new ones remain.curl -v https://example.com/ should return status 200 (or your custom response).~/Clawic/data/nginx/memory.md.~/Clawic/data/nginx/config.yaml with any new user preferences revealed (os_family, deployment, edge_position, etc.).| error message | cause | next step |
|---|---|---|
connect() failed (111: Connection refused) |
backend down, wrong IP, or wrong port | verify backend is listening (nc -zv backend-ip port); check proxy_pass upstream address; check listen port on the backend |
connect() failed (113: No route to host) |
backend IP unreachable (network/firewall) | ping the backend IP; check routing and security group rules |
no live upstreams while connecting to upstream |
all servers in the upstream marked failed (max_fails threshold hit) |
check backend error logs; lower max_fails or fail_timeout to detect recovery faster; verify the backend is actually up |
upstream prematurely closed connection |
backend crashed mid-response, sent invalid HTTP, or app timeout shorter than nginx timeout | check backend error logs for crashes; raise the app timeout if legitimate; check for connection limits on the backend |
upstream timed out (110) |
backend did not respond within proxy_read_timeout (default 60s) |
backend is slow; raise proxy_read_timeout as a band-aid only (fix the slow endpoint instead); check backend load and logs |
client intended to send too large body |
upload size exceeds client_max_body_size (default 1m) |
raise client_max_body_size in http block or the specific location to the actual max upload size; test with a file at the boundary |
worker_connections are not enough |
connection budget exhausted; active connections hit worker_connections × worker_processes limit |
increase worker_connections (usually to 4096 or 8192); increase worker_rlimit_nofile to at least 2× the new limit; see core rule 5 for capacity math |
SSL_ERROR_RX_RECORD_TOO_LONG |
browser tried TLS on a non-TLS port (e.g., port 80 with no redirect to 443) | add return 301 https://$host$request_uri; to the http server block, or listen 443 ssl; to the correct server block |
certificate verify failed |
fullchain missing intermediates or intermediate in wrong order | run openssl x509 -in /path/to/cert -text -noout and openssl s_client -connect example.com:443 -showcerts (curl's browser perspective); rebuild the chain with leaf + intermediates + root in order |
| silent timeout (no error, connection hangs) | client slow to send headers or body, or server buffering all of a streaming response before sending | increase client_body_timeout and client_header_timeout if clients are genuinely slow; set proxy_buffering off; in the location for streaming responses |
open() failed (13: Permission denied) |
file/directory not readable by the nginx user (usually www-data on Debian, nginx on RHEL) |
check ownership and permissions: ls -la /path/to/file; fix with chown nginx:nginx and chmod 644 for files, 755 for directories; on RHEL, check SELinux context too |
limit_req_zone, geo, or logs that key on $remote_addr):set_real_ip_from <CDN_CIDR>;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
X-Forwarded-Proto instead of $scheme:return 301 https://$host$request_uri; # replace with
return 301 https://$host$request_uri; # if already https at edge
# OR conditionally:
map $http_x_forwarded_proto $proto_redirect {
"https" https;
default https;
}
return 301 $proto_redirect://$host$request_uri;
limit_req_zone $binary_remote_addr now keys on the real client IP (because realip was set first), not the CDN's IP.proxy_pass trailing-slash rule (procedure step 6):proxy_pass → path passes unchanged.proxy_pass → location prefix is stripped.location /api/ + proxy_pass http://backend/; means /api/users becomes /users at the backend.rewrite before proxy_pass:location /old-api/ {
rewrite ^/old-api/(.*) /v2/$1 break;
proxy_pass http://backend; # /old-api/users → /v2/users
}
curl -v http://backend:port/path.WebSocket: add these headers in the location:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400; # long timeout for idle connections
SSE (Server-Sent Events): set proxy_buffering off; in the location (buffering delays or bunches events):
location /events {
proxy_buffering off;
proxy_pass http://backend;
}
gRPC: use grpc_pass instead of proxy_pass, and listen on http/2:
listen 443 ssl http2;
location /com.example.MyService/ {
grpc_pass grpc://backend;
}