Writing Nginx Configuration With LLMs in October 2026: Test Every Change, Get proxy_pass Slashes Right, and Add Security Headers on Purpose

Ask a model for an Nginx config and you will get something that looks right in seconds: a server block, a couple of location blocks, a proxy_pass, maybe some TLS settings. Most of the time it even loads. The trouble is that Nginx is full of small rules where one character changes behavior, such as a trailing slash, an inherited header, or an alias path. A config can pass a syntax check and still route requests to the wrong place, drop your security headers on half your pages, or expose files outside the folder you meant to serve.

Rows of servers in a data center
Image: The U.S. Food and Drug Administration via Wikimedia Commons (Public domain)

This guide walks through a practical workflow for using LLMs to write and edit Nginx configuration without shipping those mistakes. The idea is simple: let the model draft, then check the parts models most often get wrong, test before you reload, and keep a person reading the diff.

Give the model the facts it cannot guess

A model does not know your Nginx version, which modules are compiled in, what your upstream app expects, or whether TLS ends at Nginx or at a load balancer in front of it. If you leave those out, it fills the gaps with common defaults that may not match your setup. Start every request with a short brief:

  • The output of nginx -v (or nginx -V if modules matter, for example for HTTP/3 or Brotli).
  • Where TLS terminates, and whether another proxy sits in front of Nginx.
  • The upstream address and the path the app expects to receive (does it want /api/users or just /users?).
  • Whether the app uses WebSockets, server-sent events, or large uploads.
  • Your existing nginx.conf and the relevant files from conf.d or sites-enabled, with any secrets removed.

Then ask for a change to that config, not a fresh config from scratch. A small, reviewable edit is far easier to check than a whole new file that quietly drops settings you already had.

Test before every reload

Nginx ships with the most important check built in. Run it every time, no exceptions:

sudo nginx -t          # parse and validate the full configuration
sudo nginx -T | less   # same check, plus print the complete merged config
sudo systemctl reload nginx

nginx -t catches syntax errors, unknown directives, directives used in the wrong context, and missing certificate files. nginx -T is useful when a model has edited one file and you want to see how it combines with everything pulled in by include. Prefer reload over restart: on a reload, the master process starts new workers only if the new config is valid, and keeps the old workers running if it is not, so a bad edit does not take the site down.

Keep in mind what nginx -t does not do. It does not know whether a request to /api/users will reach your app as /users or /api/users, whether your headers are actually sent, or whether a path can escape the web root. Those are logic problems, and they are exactly where generated configs tend to go wrong.

Get the proxy_pass trailing slash right

This is the single most common bug in model-written reverse proxy configs. Whether proxy_pass includes a URI (anything after the host and port, even just /) changes how the request path is passed upstream.

location /api/ {
    proxy_pass http://127.0.0.1:3000;    # no URI: upstream receives /api/users
}

location /api/ {
    proxy_pass http://127.0.0.1:3000/;   # URI "/": the /api/ prefix is replaced, upstream receives /users
}

Both versions pass nginx -t. Only one matches what your app expects. When you review a generated block, ask two questions: does the location end in a slash, and does the proxy_pass include a URI? Then confirm with a real request rather than reasoning about it. A quick way is to point the upstream at a tiny echo server, or check your app's access log, and run:

curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/api/users

Also watch for mismatched slashes between the location and the proxy_pass URI. A location /api (no slash) paired with proxy_pass http://app/; turns /api/users into //users upstream, which some frameworks handle and others do not.

Check alias paths for the off-by-slash problem

Models like alias for serving static files, and they sometimes write it in a way that exposes more than intended:

# Risky: location has no trailing slash, alias does
location /static {
    alias /var/www/app/static/;
}

Because the location prefix /static also matches /static../, a request like /static../config.py can map to /var/www/app/config.py, one folder above what you meant to serve. The fix is to keep the slashes consistent, or avoid alias when root will do:

location /static/ {
    alias /var/www/app/static/;
}

# or, when the URL path matches the folder layout
location /static/ {
    root /var/www/app;
}

Make this a rule in your review checklist: every alias line gets a look at both ends.

Add security headers on purpose, and know how they inherit

Ask for "secure headers" and a model will usually add a block like this to the server context:

add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;

Two details matter. First, without the always parameter, add_header only applies to a limited set of success and redirect status codes, so your error pages go out without the headers. Second, and more surprising, add_header directives are inherited from the outer level only if the current level defines no add_header of its own. If the model later adds a single add_header Cache-Control ... inside a location block, every security header from the server block disappears for that location.

Common ways to handle this:

  • Put the security headers in a small file, such as snippets/security-headers.conf, and include it in every block that sets its own headers.
  • After any change, check the real response for a few representative paths, including a static file, an API route, and a 404:
curl -sSI https://example.com/            | grep -iE 'strict|nosniff|referrer|frame'
curl -sSI https://example.com/static/app.css | grep -iE 'strict|nosniff|referrer|frame'
curl -sSI https://example.com/missing     | grep -iE 'strict|nosniff|referrer|frame'

Be careful with headers that can break a site. A Content-Security-Policy written by a model without knowing which scripts, fonts, and analytics your pages load will often block something. Roll a CSP out with Content-Security-Policy-Report-Only first. HSTS is also hard to undo once browsers have cached it, so only add includeSubDomains or preload when you are sure every subdomain serves HTTPS.

Don't let the model invent TLS settings

TLS configuration is a place where models often reproduce settings from older tutorials: outdated protocol versions, long hand-written cipher lists, or directives that no longer exist. Rather than trusting the generated block, start from a maintained reference such as the Mozilla SSL Configuration Generator, choose your Nginx and OpenSSL versions, and paste its output. Then ask the model only to fit it into your file. Check the result from outside with a scanner such as testssl.sh or an online TLS checker.

Review the proxy headers and limits

A reverse proxy block that works on the happy path can still break your app in quieter ways. When reviewing a generated location that proxies to an app, check that it sets the headers your app relies on:

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;

If another proxy or load balancer sits in front of Nginx, $scheme and $remote_addr describe that hop, not the real client. That is when you need the real_ip module configured with only your trusted proxy addresses. Do not let a model add set_real_ip_from 0.0.0.0/0, which lets any client forge its IP address.

A few other things to look for:

  • WebSockets need proxy_http_version 1.1 plus the Upgrade and Connection headers, usually with a map on $http_upgrade.
  • Uploads fail with a 413 error above the default client_max_body_size of 1 MB. Set it where uploads actually happen instead of raising it for the whole server.
  • Streaming responses, such as server-sent events or streamed LLM output, may need proxy_buffering off for that location so tokens are not held back.
  • Server version: server_tokens off; stops Nginx from advertising its exact version in headers and error pages.

Avoid "if" where a better directive exists

Generated configs often reach for if to handle redirects or rewrites. Inside a location block, if behaves in ways that surprise people, which is why the Nginx community has long warned against it. Most cases have a cleaner answer: a separate server block with return 301 for HTTP-to-HTTPS and www redirects, try_files for single-page app fallbacks, and map for choosing a value from a request variable.

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}
Server racks in a data center
Image: The U.S. Food and Drug Administration via Wikimedia Commons (Public domain)

Run a static analyzer

Beyond nginx -t, a config analyzer can catch several of the logic bugs above automatically. gixy, originally released by Yandex, checks for issues such as the alias off-by-slash, header inheritance problems, and host header spoofing. The original project has seen little maintenance, so look for an actively maintained fork. Running it in CI on every config change turns this checklist into something that happens even on a busy day.

A review checklist for generated Nginx configs

  1. The request included your Nginx version, upstream paths, and where TLS terminates.
  2. The change is a diff against your current config, not a full rewrite.
  3. nginx -t passes, and you reload rather than restart.
  4. Every proxy_pass was checked for the trailing-slash rule and confirmed with a real request.
  5. Every alias has matching slashes on the location and the path.
  6. Security headers use always and still appear in every location that sets its own headers.
  7. TLS settings come from a maintained generator, not from the model's memory.
  8. Proxy headers, upload limits, WebSocket, and streaming settings match what the app needs.
  9. No if blocks where return, try_files, or map would work.
  10. A static analyzer ran cleanly, and a person read the diff.

Where LLMs genuinely help

Models are useful for Nginx work. They are good at explaining what an unfamiliar directive does, translating an Apache .htaccess rule into Nginx syntax, drafting a reverse proxy block for a new service, and reading an error log line to suggest why you are getting a 502. They save a lot of time looking things up in documentation.

The pattern that works is the same one that works for Dockerfiles, Terraform, and Kubernetes manifests: let the model draft, let Nginx's own test and a static analyzer validate, confirm behavior with real requests, and keep a person reading the change. The model saves you typing. The checks are what keep your site up and your files private.

One more tool worth knowing

If you're building something and want more of the right people finding it, Buyer Radar from RankReactor finds public posts on X and LinkedIn where people ask for what you sell. It scores each lead, links to the original post, and drafts a reply or email you can send yourself with one click. Nothing is sent automatically. Paid plans include it, alongside RankReactor's automated SEO articles and UGC videos.

Comments

Popular posts from this blog

Grok Bot - a step closer to AGI

Structured Outputs and Constrained Decoding for Reliable LLM APIs in Production (September 2026)

Tencent Hy4 Preview: Open 770B MoE Built for Real Work