Skip to content

Writing Criteria

Every guide in this repo is written to the same standard. If you're adding or editing a guide — human or AI — hold it to these.

  1. Plain words, real ideas. Simple language, never dumbed-down concepts. Explain jargon on first use; collect terms in the glossary.
  2. No hype. No marketing adjectives, no "revolutionary," no promises about what something will do for the reader.
  3. Honest about limits. Say what the thing can't do. State uncertainty instead of smoothing it over. Validation notes include what remains unverified.
  4. No invented numbers. Benchmarks, defaults, and specs are either measured or qualified with "depends on your setup." If it wasn't tested, say so.
  5. Checkable. Commands are meant to be run, with "Check:" blocks showing what success looks like. Sources are linked, not asserted.
  6. Respect the reader's setup. Call out Windows/Mac/Linux differences. Don't assume a GPU, a budget, or prior experience.
  7. Safety isn't skipped. Real risks get stated plainly, not buried.
  8. Scoped. Each guide says what it covers and points elsewhere for the rest. No guide pretends to be the whole subject.
  9. Structure over prose. Tables, before/after examples, symptom → fix. Scannable beats essay.

Verification notes for this guide

Everything hands-on was run for real in a loopback lab (nginx 1.24.0, Ubuntu package; ports 8080/8082/8443 on 127.0.0.1, backends on 9001/9002), as its own instance under a lab prefix; the system nginx was installed but never started, and its config is untouched. Full transcripts are the lab's receipts.

Live-captured:

  • Install (apt-get install nginx), nginx -v, the stock /etc/nginx layout, the sites-enabled symlink, and the include chain in the stock main config.
  • nginx -t succeeding on the lab config; nginx -T's expanded dump. A deliberately typo'd copy failing -t with unknown directive "limit_req_zone_broken", exit 1.
  • Master/worker process shape in ps.
  • Static serving: index, a plain file, Content-Type: text/css with mime.types included; a custom 404 page served with status 404; a directory without an index answering 403 with the directory index of ... is forbidden error-log line.
  • Location precedence proven with five fixtures: exact =, plain prefix, longer prefix, regex beating the longer prefix, ^~ suppressing the regex. Both Check predictions in chapter 6 were also run and matched.
  • root vs alias arithmetic, and the alias trailing-slash trap: 404 with open() ".../imageslogo.txt" failed in the error log.
  • try_files SPA fallback returning the index for a deep route.
  • autoindex directory listing, and a dotfile served in the unguarded configuration ("you should never see this over HTTP").
  • Reverse proxying against an echo backend: control run direct (no forwarded headers), proxied without proxy_set_header (backend sees only nginx, Host rewritten to the upstream), proxied with the header block (Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto all present). The backend's own log shows all requests arriving from 127.0.0.1, with the forwarded address only on the headered run.
  • Redirects: return 301 and a rewrite ... permanent, each with the Location header captured.
  • TLS: self-signed certificate generated with openssl (SANs DNS:localhost, IP:127.0.0.1), inspected with openssl x509; curl failing untrusted (exit 60), succeeding with -k, and succeeding with --cacert and full verification; openssl s_client negotiating TLSv1.3; the HTTP→HTTPS redirect server answering 301 with an https://...:8443 Location.
  • Failures on purpose: proxy_pass to a dead port → 502 with connect() failed (111: Connection refused) while connecting to upstream in the error log; a 5 KB POST against client_max_body_size 1k → 413.
  • Upstream pool: round-robin alternating across two backends; after the 9002 backend was stopped, all requests served by 9001, with connect() failed and upstream server temporarily disabled lines in the error log.
  • Rate limiting: 12 rapid requests → 200 200 200 then 503s, a limiting requests, excess: 2.580 by zone "labzone" error-log line, and recovery after a 2-second pause. The first fixture version (a return in the same location) did not limit at all; that negative result is captured in the working notes and taught in chapter 11 as the phase-ordering trap.
  • Reload: config edited (dotfile deny all added, index content changed), nginx -t, nginx -s reload — master PID unchanged (13739), new content served, the previously served dotfile now answering 403.
  • Access-log lines in combined format and error-log contents, quoted from the lab's own log files.

Documented-only (described, not captured), with reasons:

  • Public ACME/Let's Encrypt issuance and the certbot flow — needs a public domain, DNS control, and internet reachability the sandbox doesn't have. The server block it feeds is the captured one.
  • The filesystem-permissions 403 (open() ... failed (13: Permission denied)) — the lab's workers run as root because this sandbox refuses the chown() nginx performs for non-root worker users (uid 65534 and uid 33 attempts both captured, [emerg] chown(...) failed (1: Operation not permitted)); root is never permission-refused, so the variant is described and its log line quoted from nginx's documented format. On a stock Ubuntu install workers run as www-data and this 403 is the ordinary one.
  • NGINX Plus features (active health checks) — named as commercial, not exercised.
  • Production hardening checklists — out of scope by design (README says so); the guide covers the two exposures it could demonstrate (directory listing, dotfiles).
  • Windows/macOS — named only; nginx there is a different install story and wasn't available.
  • systemd management of the system instance (systemctl reload nginx) is stated as the package's standard behaviour; the lab deliberately never started the system service (the package's install was blocked from auto-starting by policy-rc.d, which the transcript shows).