NGINX Config Generator

DevOps engineers standing up a new service, SREs migrating a monolith behind a reverse proxy, and frontend teams deploying a static SPA all assemble the same nginx server block from half-remembered Stack Overflow answers and then debug missing X-Forwarded headers or broken hard refreshes at 2 a.m. This generator produces a complete server block for static, reverse-proxy, or SPA sites with TLS, gzip, security headers, and static caching, plus a warnings pass that catches the exact mistakes nginx will silently accept but your app will not. Review, copy, run nginx -t, reload.

Site kind
Generated server block
—

How the Generator Builds the Server Block

Options are held in a small object (site kind, hostnames, paths, feature toggles). On every change the tool re-serializes the object into a complete server { ... } block: listen directives first, then server_name (primary plus alternates, lowercased and deduplicated), TLS settings when 443 is enabled, client_max_body_size, security headers, gzip block, root/index or proxy settings, and the location / block whose body depends on the site kind. A static-location block for cacheable assets (css, js, images, fonts) is appended when a cache TTL is set, followed by a robots.txt location and access/error log paths derived from the primary hostname.

The warning pass checks the mistakes nginx accepts but applications break on. An empty server_name becomes the catch-all _, which matches every unmatched Host header — usually a mistake. proxy_pass without an absolute http:// or https:// scheme is a syntax error nginx will reject at load time. HSTS on the port-80 block is ignored by browsers; the tool warns and attaches it only to 443. For reverse-proxy sites, missing X-Forwarded-For / X-Forwarded-Proto / Host headers mean the upstream app cannot generate correct absolute URLs or secure cookies — the generator includes them by default and flags if you disable the proxy kind while leaving proxyPass set. A stable 8-hex checksum of the rendered config is shown so you can confirm two renders are identical before deploying.

location priority and try_files

nginx matches location blocks by prefix length, then regex order. The static-asset block uses a regex location (~* \\.(?:css|js|...)$) which takes precedence over the prefix location / for matching file extensions. try_files checks each file in order and falls back to the last argument; for SPA that is /index.html, for static it is =404. Getting this wrong is the most common cause of “hard refresh returns 404” on SPAs.

add_header inheritance gotcha

When a location block contains its own add_header directive, it inherits none of the parent’s add_header values — only the ones defined in that location. The generator keeps security headers at the server level and does not re-declare them inside location blocks, so they apply everywhere. If you add a custom add_header inside a location later, re-add the security headers there too or they will silently disappear for that path.

Before and after: fixing a reverse proxy that generates http:// links

Before (missing forwarded headers)
location / {
    proxy_pass http://127.0.0.1:3000;
}
# App sees Host: 127.0.0.1:3000
# Redirects go to http://example.com (wrong scheme)
# Cookies missing Secure flag
After (full forwarded headers + TLS)
location / {
    proxy_pass http://127.0.0.1:3000;
    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;
}
# App sees Host: example.com, scheme https
# Redirects and Secure cookies correct

Use Cases

Developer Production Workflow
Stand up a Node/Python app behind nginx with correct X-Forwarded headers Enable TLS, HSTS, and gzip on a new vhost in one pass Standardize server blocks across a fleet with config management
Fix SPA hard-refresh 404s with the correct try_files fallback Tune static cache TTL for fingerprinted vs non-fingerprinted assets Audit an existing config for missing security headers
Generate a baseline config for a new microservice before hardening Set client_max_body_size for file-upload endpoints Train new engineers on the mandatory reverse-proxy headers

Common Mistakes

  • Omitting proxy_set_header Host $host. The upstream receives 127.0.0.1:3000 as the Host, so absolute URL generation, cookie domains, and CSRF checks break in subtle ways.
  • Putting HSTS on the port-80 block. Browsers ignore Strict-Transport-Security over plain HTTP; it must be on the 443 listener only.
  • Using =404 for an SPA. Client-side routers need /index.html as the fallback so hard refresh on /dashboard works; =404 returns a real 404 for every client-routed path.
  • Setting immutable caching on non-fingerprinted assets. If the filename does not change on deploy (app.js, not app.3f2a.js), browsers will serve a stale copy for the full TTL; drop immutable or shorten the TTL.
  • Leaving server_name as a bare underscore (_). The catch-all matches every unmatched Host header, so probes and wrong-host requests hit your app instead of the default server.
  • Forgetting nginx -t before reload. A missing certificate path, bad proxy_pass scheme, or typo in a directive will fail at load time; always validate first, then reload (not restart) to keep existing connections alive.

Best Practices

  • Always include proxy_set_header Host, X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto for reverse-proxy sites. These four headers are the minimum for correct client IP and scheme propagation.
  • Enable TLS 1.2 and 1.3 only; disable older protocols. The generator emits ssl_protocols TLSv1.2 TLSv1.3; do not add TLSv1.0 or TLSv1.1 back for compatibility.
  • Run nginx -t before every reload, and use reload (not restart) so in-flight connections finish gracefully. Pair with a canary or blue-green deploy for zero-downtime config changes.
  • Set Cache-Control: public, immutable only for fingerprinted assets. For everything else, use a shorter TTL and let the browser revalidate with ETag or Last-Modified.
  • Keep security headers at the server level and avoid re-declaring add_header inside location blocks — doing so drops the inherited headers for that path.
  • Manage server blocks as code (Ansible, Chef, or a GitOps repo) rather than hand-editing on the box. Use this generator to produce the baseline, commit it, and let your config management push it to the fleet.

Frequently Asked Questions

What is the difference between a static, reverse-proxy, and SPA site kind?

A static site serves files directly from disk — the location / block uses try_files $uri $uri/ =404. A reverse proxy forwards requests to an upstream application (Node, Python, Go) via proxy_pass, adding headers like X-Forwarded-For and X-Real-IP so the app sees the original client. A single-page application serves index.html for any unmatched path (try_files $uri $uri/ /index.html) so client-side routing works on hard refresh. Pick the kind that matches how your app actually serves traffic.

Why does nginx need both proxy_set_header Host and X-Forwarded-Proto?

Without proxy_set_header Host $host, the upstream receives the proxy’s hostname (127.0.0.1:3000) instead of the public hostname, breaking absolute URL generation and cookie domains. Without X-Forwarded-Proto, the app cannot tell whether the original request was HTTP or HTTPS, so it may generate http:// links on a TLS site or skip secure-cookie flags. Both headers are mandatory for any reverse-proxy setup that generates redirects or sets cookies.

How do I enable HTTP/2 in the server block?

The http2 parameter on the listen directive (listen 443 ssl http2;) enables HTTP/2 on that listener. nginx 1.25.1+ deprecates the listen-level flag in favor of the separate http2 on; directive, but the listen form still works and is what most tutorials and production configs use. The generator emits the listen form for compatibility with older nginx builds; upgrade to the directive form when you confirm your nginx version is 1.25.1 or newer.

Should HSTS be on the port-80 server block?

No. Browsers only honor Strict-Transport-Security over TLS; the header is ignored on plain HTTP responses. Putting it in the port-80 block does nothing and clutters the config. The generator attaches HSTS only to the 443 listener and warns if you enable it without TLS. After verifying your site works fully on HTTPS for all subdomains, consider increasing max-age and adding preload.

Why does the SPA fallback use /index.html instead of =404?

Client-side routers (React Router, Vue Router in history mode) expect the server to return index.html for any unknown path so the JavaScript router can match the URL. With try_files $uri $uri/ /index.html, a hard refresh on /dashboard returns the app shell and the router takes over. A static site instead returns =404 for missing files so crawlers and users see real 404s; the generator picks the right form based on the site kind you select.

How do I set the static cache TTL correctly?

The location block for static assets (css, js, images, fonts) sets expires and Cache-Control: public, immutable. A 7d TTL is a safe default for assets that are fingerprinted or rebuilt on deploy. If your build does not fingerprint assets (no content hash in the filename), use a shorter TTL (1h–24h) or remove immutable so browsers revalidate after each deploy. The generator lets you change the TTL string to match your release process.

Does this tool deploy the config anywhere?

No. The generator builds the config text in your browser; nothing is uploaded or saved remotely. Review the warnings panel, then copy the server block into your sites-available file or mount it via configuration management. Always run nginx -t before reload to catch syntax errors the static checks cannot see (missing cert files, upstream down, etc.).