nginx config files grow. What starts simple gets layers added on top: another reverse proxy, TLS settings, routing logic that branches and branches again.
Before long you're looking at 20+ location blocks and can't say off the top of your head which one handles which request. You add a single add_header line and some other header silently disappears. You fix an alias path and prod throws 404s while staging is fine. nginx -t says "syntax is ok." But you don't feel ok about it.
Every config change comes with a quiet dread: will this break something? And that dread usually has a basis. nginx config mistakes are silent. Nothing shows up in the error log. The browser gets a normal-looking response. You don't find out something is broken until much later.
nginx itself is well-documented, but the ecosystem around it is harder to survey. Validation tools, config generators, test frameworks — searching turns up scattered information. This article organizes the common pain points into six categories and maps the tools that address each one.
- Problem 1: Prevent config mistakes upfront
- Problem 2: Generate config files
- Problem 3: Automate testing
Problem 1: Prevent config mistakes upfront
nginx -t checks syntax. But syntactically valid config can still have security problems. SSRF, path traversal via alias (e.g., a request to /files/../../etc/passwd reaches files it shouldn't), HTTP splitting — none of these trigger a syntax error.
Gixy: static analysis focused on security
https://github.com/dvershinin/gixy
Originally built at Yandex, now actively maintained as a community fork. It checks for well-known security risks: SSRF, alias traversal, HTTP splitting. Install the gixy-ng fork from PyPI.
pip install gixy-ng
gixy /etc/nginx/nginx.conf
Output looks like this — it tells you which directive is the problem and why:
[WARN] [http_splitting] Possible HTTP-Splitting via HTTP header.
Directive: proxy_set_header X-Forwarded-For $http_x_forwarded_for;
A good fit when you've just inherited an nginx config and want a quick security audit before touching it. Detection is limited to known patterns and there's no custom rule support, but as a first security gate in CI it offers the best return on effort.
crossplane: treat config as data
https://github.com/nginxinc/crossplane
A Python library that parses nginx config files into JSON. Useful not just for validation but for dynamically generating config or loading it in tests.
import crossplane
payload = crossplane.parse('/etc/nginx/nginx.conf')
# Check for parse errors
for error in payload['errors']:
print(f"Error: {error}")
# Walk the config tree to find specific directives
def find_directives(block, name):
for item in block:
if item.get('directive') == name:
yield item
if 'block' in item:
yield from find_directives(item['block'], name)
config = payload['config'][0]['parsed']
# Example: flag any proxy_pass that uses plain HTTP
for d in find_directives(config, 'proxy_pass'):
url = d['args'][0]
if url.startswith('http://'):
print(f"Warning: proxy_pass uses plain HTTP: {url}")
The parsed output is a tree of directives and their arguments. With a traversal like find_directives, you can express project-specific rules in Python: "are all proxy_pass values HTTPS?", "is server_tokens off set?" Where Gixy is limited to known patterns, crossplane lets you write your own.
Scripting against config files and automating structural checks in CI are where this earns its place. If you don't have a concrete need to generate or programmatically inspect config, skip it for now. Adding it "because it looks useful" tends to mean it sits unused.
Which to choose
| Goal | Pick |
|---|---|
| Find security issues quickly | Gixy |
| Manipulate or validate config in code | crossplane |
Gixy alone covers the security CI gate. If you're writing config generation or structural validation logic in Python, you need crossplane. They don't overlap, so using both is fine.
Problem 2: Generate config files
Writing TLS config from scratch means making a lot of small decisions: cipher suites, HTTP/2 support, HSTS headers. Starting from a known-good template beats hand-rolling it and getting something subtly wrong.
nginxconfig.io: generate best-practice config in the browser
https://www.digitalocean.com/community/tools/nginx
A web app by DigitalOcean that generates a complete nginx config from a series of UI choices. Select your TLS cipher suites, gzip settings, HTTP/2, security headers, and it outputs ready-to-use config files. Free, no account required.
Use it when bootstrapping a new server or bringing TLS settings up to current best practices. It's an excellent starting point, but don't deploy the output unchanged. Treat it as a template and adjust for your project's requirements.
nginx-proxy: auto-generate config in Docker environments
https://github.com/nginx-proxy/nginx-proxy
A container that watches Docker start/stop events and automatically updates nginx config. Set a VIRTUAL_HOST environment variable on your container and nginx-proxy generates the reverse proxy config for it.
services:
app:
image: my-app
environment:
- VIRTUAL_HOST=example.com
nginx-proxy:
image: nginxproxy/nginx-proxy
ports:
- "80:80"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
When app starts, nginx-proxy automatically creates config to route example.com traffic to it.
In Docker Compose setups where manually updating nginx on every deploy is friction, this removes that burden. Traefik and Caddy do the same thing. If you're not already committed to nginx (existing config, team familiarity), compare them first. If you're already running nginx, nginx-proxy is the natural fit.
Which to choose
| Environment | Pick |
|---|---|
| Regular Linux server | nginxconfig.io |
| Docker / Docker Compose | nginx-proxy |
No Docker? nginxconfig.io is enough. In Docker environments, nginx-proxy's automation pays off.
Problem 3: Automate testing
Manually sending test requests after every config change doesn't scale. Automating it means the same checks run every time.
Test::Nginx: declarative test framework
https://github.com/openresty/test-nginx
A Perl-based test framework from the OpenResty community. It starts a real nginx instance, sends requests, and checks responses. The format is declarative — you describe what config to use, what request to send, and what response to expect. Tests read clearly as a result.
=== TEST 1: basic proxy
--- config
location /api {
proxy_pass http://127.0.0.1:8080;
}
--- request
GET /api/users
--- response_body
[{"id":1}]
This fits nginx module development and verifying that config changes don't alter behavior. Perl is a real barrier for most teams today. If you're not writing custom nginx modules, hurl covers most of what you actually need.
hurl: a DSL for HTTP testing
hurl lets you write HTTP requests and assertions in a plain-text format, then run them from the shell. Easy to slot into CI. Not nginx-specific — it works for HTTP testing in general.
GET http://localhost/api/users
HTTP 200
[Asserts]
header "Content-Type" contains "application/json"
jsonpath "$.length" > 0
hurl --test api.hurl
Reach for it when you want to confirm API behavior hasn't broken after a config change, or when the team has no Perl experience. Because it's not nginx-specific, it doesn't go deep on nginx internals. It works well for endpoint reachability checks, but for testing precise nginx behavior (match precedence, path rewriting), Test::Nginx is the right tool.
Which to choose
| Situation | Pick |
|---|---|
| Testing nginx config or modules directly | Test::Nginx |
| Checking that the API behind nginx still works | hurl |
| Team has no Perl experience | hurl |
Test::Nginx for precise nginx behavior verification. hurl for HTTP interface testing with a lower adoption cost.
Problem 4: Collect metrics
Without visibility into how many requests nginx is handling and where the bottlenecks are, there's no basis for making informed decisions when problems surface.
nginx-module-vts: embedded stats module
https://github.com/vozlt/nginx-module-vts
A module that adds traffic statistics to nginx — request counts, error rates, latency per virtual host and upstream. Built in at compile time. Exposes a /status endpoint that returns JSON.
vhost_traffic_status_zone;
server {
location /status {
vhost_traffic_status_display;
vhost_traffic_status_display_format html;
}
}
Detailed traffic stats are the payoff — when you have a build environment and can compile nginx from source. Requiring a source build is the biggest barrier. If you're using a packaged nginx install, this isn't an option. If you already have Prometheus, the overhead of a custom build may not be worth it.
nginx-prometheus-exporter: bridge stub_status to Prometheus
https://github.com/nginxinc/nginx-prometheus-exporter
Reads nginx's built-in stub_status data (connection counts, request totals) and exposes it in Prometheus format. Runs as a separate process alongside nginx — no changes to nginx itself required.
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
./nginx-prometheus-exporter -nginx.scrape-uri=http://localhost/nginx_status
In Prometheus + Grafana stacks, this is the easiest way to add nginx metrics without touching nginx itself. stub_status only gives you connection counts and request totals — no per-upstream breakdown. Confirm that's enough before committing.
Which to choose
| Situation | Pick |
|---|---|
| Already running Prometheus | nginx-prometheus-exporter |
| Need per-host, per-upstream, per-status-code stats | nginx-module-vts |
| Using packaged nginx and don't want to rebuild | nginx-prometheus-exporter |
If you're on Prometheus, nginx-prometheus-exporter is by far the easier path. If you need granular metrics and control your build, look at vts.
Problem 5: Run nginx on Kubernetes
When exposing services externally in Kubernetes, you use an Ingress resource — a rule that maps incoming paths to backend services. nginx-based Ingress controllers are widely used for this. Two controllers with similar names exist, and they're frequently confused.
ingress-nginx: the Kubernetes community controller
https://github.com/kubernetes/ingress-nginx
Maintained by the Kubernetes community (kubernetes/ingress-nginx). Used as the default by many Kubernetes distributions. Large amount of documentation and community knowledge available.
In March 2025, a set of serious vulnerabilities called IngressNightmare was disclosed (CVE-2025-1974 and others, CVSS 9.8). Remote code execution was possible through the admission controller component. Fixed in versions 1.12.1 and 1.11.5. If you're running an older version, upgrade now.
On a general-purpose Kubernetes cluster running standard Ingress, this is the obvious starting point. Switching Ingress controllers later is painful, so the initial choice matters. When in doubt, ingress-nginx has fewer dead ends from a documentation standpoint.
kubernetes-ingress: the NGINX Inc. controller
https://github.com/nginxinc/kubernetes-ingress
Maintained directly by NGINX Inc. (nginxinc/kubernetes-ingress). Supports both open-source nginx and NGINX Plus (the commercial version).
The case for it is NGINX Plus features (advanced load balancing, active health checks) or a support contract with NGINX Inc. If you don't need NGINX Plus, there's little reason to choose this over ingress-nginx. For OSS nginx, the community controller has better coverage.
Which to choose
| Situation | Pick |
|---|---|
| Community resources matter | ingress-nginx |
| Planning to use NGINX Plus | kubernetes-ingress |
| Starting on EKS / GKE or similar managed cluster | ingress-nginx |
If you go with ingress-nginx, run version 1.12.1 or 1.11.5 or later. Check your current version before anything else.
Problem 6: Visualize logs
nginx access logs pile up as text. Figuring out which paths are getting hammered, or where errors are spiking, is hard to do from raw log files.
GoAccess: real-time stats in the terminal
A TUI tool that aggregates nginx access logs and displays them in real time. Graphs and stats update live in the terminal. No log server or database needed — just install and run. Can also generate HTML reports for browser viewing.
goaccess /var/log/nginx/access.log -c
# Starts in interactive mode to select the log format
# Stream live logs in COMBINED format
tail -f /var/log/nginx/access.log | goaccess --log-format=COMBINED -
Reach for it when you need answers quickly, especially on a server with no log collection infrastructure in place. It reads local log files directly, so multi-server environments are awkward. Think of it as a per-server quick-look tool.
Loki + Grafana Alloy: long-term log storage and search
https://grafana.com/oss/loki/ / https://grafana.com/docs/alloy/latest/
Loki stores and queries logs efficiently, with Grafana for dashboards. Grafana Alloy handles log collection. Alloy replaces Promtail, which reached end-of-life in March 2026 — for new setups, use Alloy.
Example Alloy config (sending nginx logs to Loki):
loki.source.file "nginx" {
targets = [{
__path__ = "/var/log/nginx/access.log",
job = "nginx",
}]
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
It earns its place when you need to aggregate logs from multiple servers, search historical logs by time range, or view nginx logs alongside other metrics in Grafana. Running the full stack yourself carries real operational overhead. Start with Grafana Cloud's free tier, or bring this in when someone on the team already knows how to operate Grafana.
Which to choose
| Situation | Pick |
|---|---|
| Need to check something right now | GoAccess |
| Already running Grafana | Loki + Grafana Alloy |
| Need long-term log retention and cross-server search | Loki + Grafana Alloy |
| Debugging with direct SSH access to the server | GoAccess |
GoAccess gives you immediate answers. Loki + Alloy is for ongoing operations. They're not mutually exclusive — GoAccess for fast situational awareness, Loki for accumulating and analyzing over time.
Summary: match tool to problem
| Problem | Tool |
|---|---|
| Find security risks in config | Gixy |
| Manipulate config programmatically | crossplane |
| Generate correct config (Linux server) | nginxconfig.io |
| Auto-manage Docker reverse proxies | nginx-proxy |
| Test nginx config behavior | Test::Nginx |
| Add HTTP connectivity checks to CI | hurl |
| Collect metrics via Prometheus | nginx-prometheus-exporter |
| Need detailed per-upstream stats (module) | nginx-module-vts |
| Set up Ingress on Kubernetes | ingress-nginx (1.12.1+) |
| Need NGINX Plus features | kubernetes-ingress |
| Visualize logs immediately | GoAccess |
| Long-term log storage and search | Loki + Grafana Alloy |
nginx itself is stable and mature, but the ecosystem moves fast — Promtail's EOL is a good example. Periodically check the maintenance status of tools you rely on, or you'll find out about a support cutoff at the worst time.
Gaps that still aren't filled
The tools covered here leave some problems unsolved or underserved. If you know tools that address any of these, drop them in the comments.
Dynamic upstream management
Updating upstreams without an nginx reload is a common requirement. With open-source nginx, the only options are OpenResty (Lua scripting for dynamic upstream control) or NGINX Plus (API-driven management). Vanilla nginx requires rewriting config and reloading.
Config drift tracking across environments
There's no standard tool for tracking and visualizing how nginx config differs between dev, staging, and production. Git-based management is the practical answer, but cross-environment diff review and detecting prod-only config still means manual work.
IDE / LSP support
VSCode extensions for nginx.conf exist, but completion and inline validation are incomplete. No editor today reliably explains location block match priority or how proxy_pass transforms paths.
WAF
ModSecurity has a solid track record as an nginx WAF, but rule management and false-positive tuning are heavy ongoing work. There's nothing you can drop in and operate at a low maintenance level.
https://github.com/owasp-modsecurity/ModSecurity-nginx
Distributed tracing
The nginx OpenTelemetry module (ngx_otel_module) shipped in 2023 but is still maturing. Trace collection with Jaeger or Tempo is possible, but documentation and real-world operational experience are thin.