Nginx Config skill for Codex
Right headers, right slashes, nginx -t first. Codex writes Nginx server blocks that forward host, scheme and client IP correctly, keep upstream connections alive, proxy WebSockets and SSE, and terminate TLS 1.2 and 1.3 with HSTS. It knows the proxy_pass trailing-slash table and the add_header inheritance trap, adds caching and limit_req where they help, and reloads only after nginx -t passes and you confirm.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- nginx-config Installs to ~/.agents/skills/nginx-config/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install nginx-config The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project nginx-config Installs to .agents/skills/nginx-config/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/nginx-config.zipunzip -o nginx-config.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) nginx-config PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $nginx-config, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes and debugs Nginx configuration - reverse proxy with correct proxy_pass slash semantics and forwarded headers, upstream keepalive, WebSocket and SSE proxying, TLS 1.2/1.3 with HSTS, real client IP behind load balancers, proxy and static caching, limit_req rate limiting, SPA fallbacks, and safe nginx -t reloads. Use when putting Nginx in front of an app, fixing 502/504 errors, redirect loops or wrong client IPs, adding HTTPS, caching or rate limits, or reviewing an nginx.conf or sites-available file.
Use it when
- An app needs a reverse proxy, TLS termination or static file serving
- Users hit 502, 504, 413 errors or redirect loops through Nginx
- App logs show the proxy's IP instead of the client's
- You need caching or rate limiting at the edge of a service
Use something else when
- You want automatic certificates with minimal config: use caddy-server
- The issue is certificate issuance or renewal itself: use tls-certificates
How to check the result
nginx -t passes, curl shows the HTTP to HTTPS redirect and HSTS, openssl s_client negotiates TLS 1.3 with a full chain, and app logs record real client IPs.
The mistake it prevents
Adding one add_header inside a location block, which silently drops every security header inherited from the server block.
A good Nginx config is small, tested with nginx -t before every reload, passes the
original host, scheme and client IP to the app, terminates TLS with modern defaults, and
avoids the traps that only show in production: proxy_pass slash rewriting, add_header
inheritance, and if inside location.
Inspect first
- Find the layout:
nginx -V 2>&1 | tr ' ' '\n' | grep -E 'conf-path|with-http_v2|with-http_v3'andnginx -Tto dump the full effective config (includes resolved). - Note the version (
nginx -v).http2 on;needs 1.25.1+; older versions uselisten 443 ssl http2;. - Identify what sits in front (cloud load balancer, CDN) and behind (app port, socket).
- Read
AGENTS.mdfor the app’s health endpoint and whether it expectsX-Forwarded-Prototo build URLs.
Reverse proxy baseline
upstream app {
server 127.0.0.1:3000;
keepalive 32;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' '';
}
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ { root /var/www/acme; }
location / { return 301 https://example.com$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
client_max_body_size 20m;
location / {
proxy_pass http://app;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 60s;
}
}
keepalivein the upstream only works withproxy_http_version 1.1and aConnectionheader that is notclose; the map sends an empty value for normal requests.- Start HSTS with a short
max-age(300) when the site is new to HTTPS; addpreloadonly when every subdomain serves HTTPS permanently. - Let’s Encrypt stopped OCSP responses in 2025, so
ssl_staplingdoes nothing for its certificates; leave it off.
proxy_pass slash rules
| location | proxy_pass | Request /api/users reaches upstream as |
|---|---|---|
/api/ | http://app | /api/users (URI passed unchanged) |
/api/ | http://app/ | /users (location prefix replaced by /) |
/api/ | http://app/v2/ | /v2/users |
/api | http://app/ | //users (double slash: match prefix and path slashes) |
A variable in proxy_pass (proxy_pass http://$backend;) disables this rewriting and
requires a resolver directive for hostnames.
Real client IP behind a load balancer
set_real_ip_from 10.0.0.0/8; # your LB or CDN ranges only
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Never trust X-Forwarded-For from everywhere; any client can forge it. For Cloudflare,
use its published IP ranges and CF-Connecting-IP.
Rate limiting
# http context
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_status 429;
# server or location
location /api/ { limit_req zone=api burst=20 nodelay; proxy_pass http://app; }
location = /login { limit_req zone=login burst=5; proxy_pass http://app; }
- 10 MB of zone holds about 160,000 IPv4 states.
- Behind a load balancer without the real-IP block above, every client shares the LB’s address and one user exhausts the limit for everyone.
- Use
limit_req_dry_run on;first to log what would be rejected.
Caching
# http context
proxy_cache_path /var/cache/nginx/app levels=1:2 keys_zone=appcache:50m max_size=2g inactive=60m use_temp_path=off;
location /public/ {
proxy_cache appcache;
proxy_cache_valid 200 301 10m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status always;
proxy_pass http://app;
}
location ~* \.(?:js|css|woff2|png|jpg|svg|webp)$ {
root /var/www/app/dist;
expires 1y;
add_header Cache-Control "public, immutable" always;
}
- Nginx does not cache responses carrying
Set-CookieorCache-Control: private; that is usually correct. Never cache authenticated pages by ignoring those headers. immutableonly for fingerprinted filenames.
Common locations
location / { try_files $uri $uri/ /index.html; } # SPA fallback (static root)
location /events/ { # Server-Sent Events
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_read_timeout 1h;
}
location = /healthz { access_log off; proxy_pass http://app; }
Hard rules
add_headerin alocationdiscards everyadd_headerfrom the parent level. Repeat security headers in each location that adds its own, or put them in an included snippet.- Use
alwayson headers that must appear on error responses too. - Avoid
ifinlocationexcept forreturnandrewrite ... last; usemapinstead. server_name _;default server shouldreturn 444;so unknown Host headers do not hit the app.- Do not expose
/.git,.envor backup files from a static root:location ~ /\.(?!well-known) { deny all; }.
Debugging
| Symptom | Check |
|---|---|
| 502 Bad Gateway | App down or wrong port; error_log shows connect() failed; SELinux httpd_can_network_connect |
| 504 Gateway Timeout | Slow upstream; raise proxy_read_timeout only after confirming the app is not stuck |
| Redirect loop | App sees http because X-Forwarded-Proto missing, redirects to https forever |
| 413 Request Entity Too Large | client_max_body_size |
| Wrong client IP in app logs | Missing real-IP config or proxy headers |
| Old config still served | Reload failed silently; run nginx -t and check systemctl status nginx |
curl -sv -H 'Host: example.com' https://127.0.0.1/ -k tests a vhost locally;
curl -I shows headers.
Apply
sudo nginx -t && sudo systemctl reload nginx
reload keeps existing connections; restart drops them. On a production host, show the
diff and the nginx -t result and get the user’s confirmation before reloading.
Verification
nginx -tpasses;nginx -Tshows the intended effective config.curl -I http://example.comreturns 301 to https;curl -I https://example.comshows HSTS and expected headers.openssl s_client -connect example.com:443 -servername example.com </dev/nullshows TLS 1.3 and the full chain.- App logs show real client IPs; a burst test returns 429 at the configured rate.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Nginx Config"
short_description: "Right headers, right slashes, nginx -t first"
default_prompt: "$nginx-config Put Nginx in front of my Node app on port 3000 with HTTPS" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.