Networking & web 24 entries 14 run in the sandbox

curl and nginx cheat sheet

Probe endpoints with curl and run nginx as a safe reverse proxy. Each entry has a worked example and the flags worth knowing, and where one is easy to get wrong, the gotcha.

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.

Status codes on call

What the status code tells you

Map an HTTP status to the layer that most likely produced it.

4xx: the request was rejected. 5xx: the server or something behind it failed. At a proxy, 502 and 504 almost always point at the upstream.

401 vs 403
401: not authenticated (missing or bad credentials). 403: authenticated but not allowed, or a file permission problem.
404
Wrong path, wrong virtual host, or a routing rule that does not match.
413
Body too large; raise client_max_body_size in nginx.
429
Rate limited. Respect Retry-After.
499
nginx-specific: the client closed the connection before the response.
500
Unhandled application error; read the application log.
502
The proxy could not get a valid response: upstream down, refused or crashed.
503
Deliberately unavailable: overload, maintenance or no healthy backends.
504
The upstream accepted the connection but did not answer in time.

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.

Your pager is ready.

Free, instant, and it works on your phone. No signup: start as a guest and save your progress later.