agentleFS
TheDecipheristTheDecipherist/claude-code-mastery-project-starter-kit/.claude/skills/nginx/SKILL.md

nginx

Skill337 starsChanged 5 months agoMITView on GitHub

Production NGINX configuration best practices, especially as a reverse proxy in front of containerized backends. Use when writing or editing nginx.conf, server blocks, upstreams, SSL, proxy caching, security headers, structured logging, or stream (TCP/UDP) proxying. Covers the Docker-DNS resolver that keeps upstreams from going stale, separate access-controlled health ports, the stream-block placement gotcha, and headers that must be sent on errors too. Kept separate from the docker and docker-swarm skills.

10 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

1 rule. 0 are common, 1 is distinctive, and 0 are very specific to this project.

  • 0

    Common

    Most of the 9 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Keep health checks and metrics off the production port: different access control, no log noise, no interference with real traffic.

    line 63

“Common” is measured: kept by at least 6 in 10 of the 9 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

NGINX: Production Reverse-Proxy Config

Aimed at NGINX in front of containerized backends. The defaults are fine for a static site, these are the things that bite in production.

Resolve upstreams through Docker DNS with a short TTL

By default NGINX resolves an upstream host once, at startup, and caches the IP forever. In Docker that IP belongs to a container that will be replaced, so the proxy keeps sending traffic to a dead address. Point NGINX at Docker's internal DNS and force re-resolution:

http {
    resolver 127.0.0.11 ipv6=off valid=10s;   # Docker DNS, re-resolve every 10s
}

valid=10s is what makes NGINX pick up the new container after a restart or scale. This is the NGINX side of "never hardcode IPs", see the docker-swarm skill for the principle.

Upstreams by service name, with keepalive

Reference backends by service name, never IP. Reuse connections with keepalive, which needs HTTP/1.1 and a cleared Connection header:

upstream backend {
    server backend-service:8080;
    keepalive 32;
}
server {
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Structured JSON logs to stdout/stderr

Log JSON so an aggregator can parse it, and write to stdout/stderr so Docker's logging driver captures it. Never log to a file inside the container.

log_format json_log escape=json '{'
    '"time":$msec,"method":"$request_method","status":$status,'
    '"uri":"$request_uri","rt":$request_time,'
    '"upstream":"$upstream_addr","cache":"$upstream_cache_status",'
    '"client":"$remote_addr","xff":"$http_x_forwarded_for"'
'}';
access_log /dev/stdout json_log;
error_log  /dev/stderr warn;

Health and status on separate, access-restricted ports

Keep health checks and metrics off the production port: different access control, no log noise, no interference with real traffic. Restrict to internal networks and turn off access logging.

server {                          # load balancer health check
    listen 82;
    allow 10.0.0.0/8; allow 172.16.0.0/12; allow 127.0.0.1; deny all;
    location /health { access_log off; return 200 "OK"; }
}
server {                          # stub_status for Prometheus/Datadog
    listen 81;
    allow 10.0.0.0/8; allow 127.0.0.1; deny all;
    location /nginx_status { stub_status on; }
}

A deep health check that proxies an upstream's own /health is worth a third port when a service's liveness depends on its backend being reachable.

Stream (TCP/UDP) blocks go OUTSIDE the http block

Proxying a non-HTTP protocol like MongoDB or a database uses the stream module, which is a top-level block, not inside http. Putting it inside http is a silent misconfiguration. HTTP services (an Elasticsearch REST proxy, say) stay inside http.

load_module modules/ngx_stream_module.so;
include /etc/nginx/mongo.conf;    # stream { ... }  OUTSIDE http
http {
    include /etc/nginx/elasticsearch.conf;   # HTTP proxy, INSIDE http
}

SSL and security headers

Modern protocols and ciphers, session cache, OCSP stapling. When certs come from Docker secrets they are mounted at /run/secrets/<name>:

ssl_certificate     /run/secrets/server_pem;
ssl_certificate_key /run/secrets/server_key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:60m;
ssl_stapling on; ssl_stapling_verify on;

Send security headers with always so they are present on error responses too, not just 2xx/3xx:

add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Always add a Content-Security-Policy, this is the one that gets skipped

CSP is the highest-value security header and the one almost always left out. The others harden edges; CSP is the primary defense against XSS and content injection, it tells the browser which sources are allowed to load scripts, styles, images, and frames, so an injected <script> from an attacker simply doesn't execute. A site without a CSP has no second line of defense once markup injection gets through. Add it by default, do not wait to be asked.

default-src 'self' alone is technically a CSP but it breaks most real apps (CDNs, inline styles, analytics) and lulls you into thinking you're covered, so set the directives explicitly:

add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;

Rules that matter:

  • Avoid 'unsafe-inline' and 'unsafe-eval' in script-src. They re-open the XSS hole CSP exists to close. If you have inline scripts, use a per-request nonce or a hash, not a blanket unsafe allow.
  • object-src 'none' and base-uri 'self' are free wins that block plugin and base-tag injection. Set them every time.
  • frame-ancestors controls who can iframe you and supersedes X-Frame-Options, so put your clickjacking policy here.
  • Roll out with report-only first. A too-strict CSP breaks the page silently. Ship Content-Security-Policy-Report-Only to collect violations without enforcing, watch what trips, tighten, then promote to the enforcing header. The real policy is app-specific and is built by tuning, not guessed in one line.

Proxy caching for read-heavy upstreams

Cache GET/HEAD, and serve stale on upstream error or timeout so a backend hiccup doesn't reach users:

proxy_cache es_cache;
proxy_cache_methods GET HEAD;
proxy_cache_valid 200 1m;
proxy_cache_key $host$uri$args;
proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
proxy_hide_header X-Powered-By;
add_header X-Proxy-Cache $upstream_cache_status;

Watch line endings in config files

NGINX config copied in with Windows CRLF line endings can fail to parse or behave oddly in a Linux container, which is why production NGINX images often run dos2unix on the configs at build time. If nginx -t reports something that makes no sense, check the line endings first, see the dev-pitfalls skill.


This skill is built to grow. Add a directive when a real production NGINX problem has a stable, defensible fix. ModSecurity/WAF setup (build as a dynamic module, load_module) is deep enough to deserve its own section when needed.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.