Probe with curl
curl -I <url>
Fetch response headers only (a HEAD request): status, redirects, caching and server headers.
curl -I https://example.com
HTTP/2 200
content-type: text/html; charset=UTF-8
cache-control: max-age=604800
-I- HEAD request; print headers.
-i- GET, and include headers above the body.
Gotcha Some apps handle HEAD differently from GET. If -I looks fine but users see errors, test with -i or -v.
curl -v <url>
Show the whole exchange: DNS result, TCP connect, TLS handshake, request headers and response headers.
curl -v http://localhost:8080/healthz
* Trying 127.0.0.1:8080...
* connect to 127.0.0.1 port 8080 failed: Connection refused
curl: (7) Failed to connect to localhost port 8080
-v- Verbose; lines starting with > are sent, < are received.
--trace-ascii FILE- Full byte-level trace.
Gotcha curl exit 7 is 'connection refused', 28 is 'timeout', 6 is 'could not resolve host', 60 is a certificate problem.
curl -s -o /dev/null -w '%{http_code}' <url>
Print only the status code, for health checks, loops and scripts.
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/healthz
200
-s- Silent: no progress meter.
-o /dev/null- Discard the body.
-w FORMAT- Print variables after the transfer.
-f / --fail- Exit non-zero on HTTP 400 and above.
curl -w '<timing vars>' <url>
Break down where a slow request spends its time: DNS, TCP connect, TLS, first byte or transfer.
curl -s -o /dev/null -w 'dns %{time_namelookup} connect %{time_connect} tls %{time_appconnect} ttfb %{time_starttransfer} total %{time_total}\n' https://example.com
dns 0.004 connect 0.021 tls 0.064 ttfb 1.912 total 1.915
time_namelookup- Seconds until DNS resolved.
time_connect- Until the TCP handshake completed.
time_appconnect- Until TLS completed.
time_starttransfer- Until the first response byte: the server's thinking time.
Gotcha Each value is cumulative from the start. Here nearly two seconds pass between TLS and the first byte, so the backend is slow, not the network.
curl -L <url>
Follow redirects to the final response.
curl -sIL http://example.com
-L- Follow Location headers.
--max-redirs N- Stop after N hops, to catch redirect loops.
curl -H '<Header: value>' <url>
Send custom headers, such as an auth token or an Accept type.
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/v1/me
-H- Add or replace a request header; repeat for more.
-u user:pass- HTTP basic auth.
-A UA- Set the User-Agent.
Gotcha Tokens in command lines land in shell history and process lists. Prefer -H @headers.txt or an environment variable.
curl -X POST -d <body> <url>
Send a request body, for example JSON to an API.
curl -X POST -H 'Content-Type: application/json' -d '{"name":"smoke-test"}' https://api.example.com/v1/items
-X METHOD- Request method; -d already implies POST.
-d DATA- Body; -d @file.json reads it from a file.
--json DATA- curl 7.82+: body plus JSON Content-Type and Accept.
curl --resolve <host>:<port>:<ip> <url>
Send a request to a specific server while keeping the real host name for SNI, certificates and virtual hosts.
curl --resolve api.example.com:443:10.0.1.50 https://api.example.com/healthz
--resolve H:P:IP- Pin H:P to IP for this request only.
--connect-to H:P:H2:P2- Redirect the connection to another host and port.
Gotcha This is how you test a new server or a single backend before switching DNS. No /etc/hosts edits to forget.
curl -H 'Host: <name>' http://<ip>/
Hit a server by IP while selecting a name-based virtual host (plain HTTP).
curl -H "Host: shop.example.com" http://10.0.1.50/
-H Host:- Overrides the virtual host; use --resolve for HTTPS so SNI matches.
curl http://127.0.0.1:<app-port>/
Call the application directly on the host, skipping the proxy, to learn which layer is failing.
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/healthz
app OK, proxy 502- Check the proxy upstream address, port and timeouts.
app refused- The app is down or listening elsewhere; check ss -tlnp.
curl --max-time <s> --retry <n> <url>
Bound how long a probe can hang and retry transient failures, so scripts fail fast.
curl -fsS --connect-timeout 3 --max-time 10 --retry 3 https://example.com/healthz
--connect-timeout S- Limit only the connection phase.
--max-time S- Limit the whole operation.
--retry N- Retry transient errors N times.
-fsS- Fail on HTTP errors, stay quiet, but still print errors.
curl --cacert <ca.pem> <url>
Trust a private CA for one request instead of disabling certificate verification.
curl --cacert /etc/ssl/internal-ca.pem https://vault.internal:8200/v1/sys/health
--cacert FILE- CA bundle to verify the server with.
-k- Skip verification entirely; for a quick diagnosis only.
Gotcha curl -k in scripts turns every future man-in-the-middle into a silent success. Fix the trust chain instead.
openssl s_client -connect <host>:443
Inspect the certificate a server actually presents: subject, issuer and expiry dates.
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | openssl x509 -noout -subject -dates
subject=CN = example.com
notBefore=Aug 1 00:00:00 2026 GMT
notAfter=Oct 30 23:59:59 2026 GMT
-servername- Send SNI; without it you may get the default certificate.
-showcerts- Print the whole chain, to spot a missing intermediate.
x509 -noout -dates- Print validity dates only.
nginx
nginx -t
Test the configuration syntax and referenced files before reloading.
sudo nginx -t && sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
-t- Test and exit.
-T- Test and print the complete merged configuration.
-c FILE- Use an alternative configuration file.
Gotcha Chain the reload with && so a broken config never gets applied.
nginx -T
Dump the effective configuration with every include expanded, to find which server block really handles a name.
sudo nginx -T | grep -n "server_name"
-T- Print every configuration file nginx loads.
tail -n 50 /var/log/nginx/error.log
Read nginx's own error log. Each common message points at a specific cause.
2026/09/23 09:42:10 [error] 813#813: *4121 connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://127.0.0.1:8080/"
(111: Connection refused)- Nothing listens on the upstream address. The app is down or on another port.
upstream timed out (110)- The app is too slow; raise proxy_read_timeout only after checking why.
(13: Permission denied)- nginx cannot read a file or directory, or SELinux blocks it.
no live upstreams- Every server in the upstream block is marked failed.
Reverse proxy server block
Proxy a public name to a local app with the headers the app needs to see the real client and scheme.
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
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_pass- Upstream URL. A trailing path or slash changes how the location prefix is rewritten.
Host $host- Pass the original host name instead of 127.0.0.1.
X-Forwarded-For- Client IP chain for logs and rate limits.
X-Forwarded-Proto- Lets the app build https:// links behind TLS termination.
Gotcha proxy_pass http://app:8080/ (with slash) strips the matched location prefix; without the slash it is kept. Mixing these up causes 404s.
Upstream pool
Load-balance across several app instances with passive health checks and connection reuse.
upstream app {
least_conn;
server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
keepalive 32;
}
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
least_conn- Send new requests to the least busy server.
max_fails / fail_timeout- Mark a server down after failures within the window.
keepalive N- Idle upstream connections to keep open.
proxy_http_version 1.1- Required for upstream keepalive, together with the empty Connection header.
Proxy timeouts & limits
The directives behind most 504 and 413 responses.
client_max_body_size 20m;
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
client_max_body_size- Largest request body; exceeding it returns 413 (default 1m).
proxy_connect_timeout- Time to open the upstream connection.
proxy_read_timeout- Maximum gap between two reads from the upstream (default 60s).
Gotcha Raising timeouts hides slowness and ties up workers. Find out why the upstream is slow first.
Access log with upstream timing
Log request time, upstream time and upstream address so slow requests can be attributed.
log_format timed '$remote_addr [$time_local] "$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time ua=$upstream_addr';
access_log /var/log/nginx/access.log timed;
$request_time- Total time nginx spent on the request.
$upstream_response_time- Time spent waiting for the upstream.
$upstream_addr- Which backend served it.
Rate limiting
Limit requests per client IP to protect an expensive endpoint, allowing a small burst.
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;
proxy_pass http://app;
}
zone=api:10m- Shared memory for about 160,000 client states.
rate=10r/s- Sustained rate per key.
burst=20 nodelay- Absorb spikes of 20 without queueing delay.
limit_req_status 429- Return 429 instead of the default 503.
Gotcha Behind a load balancer every request comes from the balancer's IP. Configure set_real_ip_from and real_ip_header first.
Redirect HTTP to HTTPS
Send every plain HTTP request to HTTPS with a permanent redirect.
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
301- Permanent; browsers cache it. Use 302 while testing.
$request_uri- Original path and query string.
certbot renew --dry-run
Check that Let's Encrypt certificates will renew automatically before one expires on a Friday night.
sudo certbot renew --dry-run
--nginx -d NAME- Obtain a certificate and wire it into nginx.
certificates- List certificates and expiry dates.
--dry-run- Test against the staging environment without saving.