Skip to content

Repository files navigation

nginx-lint

License: MIT

A linter for nginx configuration files with WASM plugin support, autofix, and a browser-based Web UI.

Features

  • 30+ built-in rules covering security, best practices, style, syntax, and deprecation
  • Autofix — automatically fix problems with --fix
  • WASM plugin system — extend with custom rules written in Rust and compiled to WebAssembly
  • Web UI — lint interactively in the browser with real-time feedback
  • Ignore comments — suppress specific warnings with inline annotations
  • Configurable — customize rules, severity, and options via .nginx-lint.toml
  • JSON output — machine-readable output for CI integration
  • GitHub Actions integration — inline PR annotations with --format github-actions

Documentation

For a comprehensive getting started guide covering installation, configuration, CI integration, and more, see docs/guide.md or run nginx-lint guide.

Quick Start

You can also try it in the browser: Demo

# Lint a configuration file
nginx-lint /etc/nginx/nginx.conf

# Automatically fix problems
nginx-lint --fix /etc/nginx/nginx.conf

# Show why a rule exists
nginx-lint why server-tokens-enabled

# List all available rules
nginx-lint why --list

Docker

# Lint a configuration file (replace TARGET_PATH with your config path)
TARGET_PATH=/etc/nginx/nginx.conf docker run --rm -v "$(dirname "$TARGET_PATH"):$(dirname "$TARGET_PATH"):ro" ghcr.io/walf443/nginx-lint:latest "$TARGET_PATH"

# Automatically fix problems (mount as read-write)
TARGET_PATH=/etc/nginx/nginx.conf docker run --rm -v "$(dirname "$TARGET_PATH"):$(dirname "$TARGET_PATH")" ghcr.io/walf443/nginx-lint:latest --fix "$TARGET_PATH"

Usage

nginx-lint [OPTIONS] [FILE]...
nginx-lint <COMMAND>

Options

Flag Description
-o, --format <FORMAT> Output format: errorformat (default), json, or github-actions
--fix Automatically fix problems
-c, --config <FILE> Path to configuration file
--context <CONTEXT> Parent context for partial configs (e.g., http,server)
--plugins <DIR> Directory containing custom WASM plugins
--color / --no-color Force or disable colored output
--no-fail-on-warnings Only fail on errors, not warnings
-v, --verbose Show verbose output
--profile Show time spent per rule

Subcommands

config — Configuration file management

nginx-lint config init                  # Generate default .nginx-lint.toml
nginx-lint config init -o custom.toml   # Custom output path
nginx-lint config validate              # Validate configuration

why — Show detailed documentation for a rule

nginx-lint why server-tokens-enabled    # Explain a rule
nginx-lint why --list                   # List all rules

Configuration

Generate a default configuration file with:

nginx-lint config init

This creates .nginx-lint.toml:

[color]
ui = "auto"       # "auto", "always", or "never"
error = "red"
warning = "yellow"

[rules.server-tokens-enabled]
enabled = true

[rules.indent]
indent_size = "auto"   # or a number like 4

[rules.deprecated-ssl-protocol]
allowed_protocols = ["TLSv1.2", "TLSv1.3"]

# Support non-standard directives from extension modules
[rules.invalid-directive-context]
additional_contexts = { server = ["rtmp"], upstream = ["rtmp"] }

[parser]
block_directives = ["rtmp", "application"]

Rules

See the rules list for all available rules, or run nginx-lint why --list locally.

Ignore Comments

Suppress warnings using nginx-lint:ignore comments. Both a rule name and a reason are required.

Comment on the line before:

# nginx-lint:ignore server-tokens-enabled required by monitoring system
server_tokens on;

Inline comment:

server_tokens on; # nginx-lint:ignore server-tokens-enabled required by monitoring system

Context Comments

When linting partial configuration files (e.g., included snippets), specify the parent context:

# nginx-lint:context http,server
location /api {
    proxy_pass http://backend;
}

This is equivalent to --context http,server on the command line.

Include Resolution

nginx-lint automatically follows include directives and lints the included files as well. Both absolute paths and glob patterns (e.g. include /etc/nginx/conf.d/*.conf;) are supported.

Path Mapping

In production, nginx configs often include files from a directory that is populated at runtime via symlinks (e.g. sites-enabled/), while the actual source files live elsewhere (e.g. sites-available/). You can configure path mappings in .nginx-lint.toml so that nginx-lint reads from the correct location:

[[include.path_map]]
from = "sites-enabled"
to   = "sites-available"

With this configuration, an include sites-enabled/*.conf; directive will be resolved as sites-available/*.conf during linting.

Key behaviors:

  • Component-level matching — from is matched against exact path segments, so sites-enabled will not match asites-enabled or sites-enabled-old.
  • Multi-segment values — from = "nginx/sites-enabled" matches consecutive path components.
  • Chained application — Multiple [[include.path_map]] entries are applied in declaration order, with each mapping receiving the output of the previous one.
# Chained example: sites-enabled → sites-available → conf
[[include.path_map]]
from = "sites-enabled"
to   = "sites-available"

[[include.path_map]]
from = "sites-available"
to   = "conf"

Web UI

Try the Web UI online without installation: Demo

Start the browser-based linting interface locally:

nginx-lint web --open

The Web UI provides:

  • Real-time linting as you type
  • Interactive fix buttons for each issue
  • "Fix All" to apply all fixes at once
  • Rule documentation with bad/good examples
  • In-browser configuration editing
  • Runs entirely client-side via WebAssembly

GitHub Actions

You can use nginx-lint-action to run nginx-lint in your GitHub Actions workflow with inline PR annotations:

- uses: walf443/nginx-lint-action@v1
  with:
    files: /etc/nginx/nginx.conf

Alternatively, use --format github-actions directly to produce workflow commands:

nginx-lint --format github-actions /etc/nginx/nginx.conf

Custom Plugins

Load custom WASM plugins from a directory:

nginx-lint --plugins ./my-plugins /etc/nginx/nginx.conf

Each .wasm file in the directory is loaded as a plugin, in file name order. A file carries one rule, or several: a component built against the plugin-rules world exports a list of rules, which the host loads as though each had come from its own file. Every SDK builds that world (a single rule is a list of one). Components built against the original plugin world, one rule per file, keep loading — with a warning: a future major release will stop loading them, and rebuilding with a current SDK is the fix.

There is an nginx-lint-plugin SDK for four languages, each with a worked example beside it, and a builder for Lua scripts:

Language SDK Example
Rust crates/nginx-lint-plugin plugins/builtin/ (one rule each), plugins/rust/security-rules (two rules in one component)
TypeScript nginx-lint-plugin (plugins/typescript/nginx-lint-plugin) plugins/typescript/server-tokens-enabled-ts
Python nginx-lint-plugin (plugins/python/nginx-lint-plugin) plugins/python/server-tokens-enabled-py
Go plugins/go/nginx-lint-plugin plugins/go/server-tokens-enabled-go
Lua nginx-lint-plugin-sdk binary (plugins/nginx-lint-plugin-sdk) plugins/lua/server-tokens-enabled-lua

Every library SDK can run a plugin against the real parser from its own test suite; a Lua plugin's check runs only under nginx-lint test-plugins. Go plugins additionally need --allow-wasi-plugins, for the reason described below.

Lua needs no toolchain at all: nginx-lint-plugin-sdk build my_rule.lua turns a script into a plugin by writing it into a prebuilt Lua runtime that the binary carries, and the result runs under the default sandbox. See plugins/nginx-lint-plugin-sdk/README.md for the script API.

Testing a plugin

test-plugins runs every plugin in a directory against the examples it carries in its own spec — the ones nginx-lint why renders — so there is nothing to point it at but the directory:

nginx-lint test-plugins --plugins ./my-plugins

For each plugin it requires the bad example to be reported, the good example to be clean, and the fixes to resolve the bad example. A rule with no autofix skips the last one rather than failing it. It exits 1 if any check fails, and 2 if a plugin could not be loaded — the linter warns and carries on there, which is right for linting and wrong for a command asked to check the plugins.

Findings are matched by the plugin's own rule name, so several plugins can share a directory, and a rule that is disabled by default or turned off in .nginx-lint.toml is still testable — which --rule-only cannot do.

If the plugin follows the fixture layout the SDKs document, point at it too:

nginx-lint test-plugins --plugins . --fixtures tests/fixtures

Each <case>/error/nginx.conf has to be reported and each <case>/expected/nginx.conf has to be clean; a case may declare only one of the two. The cases are written for one rule. When the directory loads more than one — a component can carry several — each rule's cases live one level down, under a directory named after the rule (tests/fixtures/<rule>/<case>/); a rule without one gets the example checks only.

The plugin sandbox

Plugins run as WebAssembly components with no WASI: no filesystem, no network, no environment, no clock. They see only the configuration the linter hands them, and a plugin importing wasi:* fails to load. Memory is capped at 256 MB and a single check has a 10-second deadline.

Some toolchains cannot produce a plugin without WASI imports at all — Go, via componentize-go, adapts a wasip1 module, so its runtime pulls in stdio, environment, clocks and randomness even for a plugin that only reads the config it is given. --allow-wasi-plugins loads those:

nginx-lint --plugins ./my-plugins --allow-wasi-plugins /etc/nginx/nginx.conf

It links a subset of WASI backed by an empty context, so it grants no filesystem, network, environment or terminal access, and wasi:sockets/* is never linked at all. What every plugin loaded does gain is a clock and randomness — so a plugin can behave differently from one run to the next — and the ability to block indefinitely inside a WASI call: the 10-second deadline interrupts wasm execution, not host calls, so a plugin that subscribes to a duration it chooses holds the thread for exactly that long. Both are reasons to leave the flag off unless a plugin needs it.

A project whose plugins need it on every run can set it in .nginx-lint.toml instead:

[plugins]
allow_wasi_plugins = true

The flag and the setting are OR'd, and there is deliberately no way to say no from the command line. Which plugins to load stays a command line decision — [plugins] has no directory key, and never will — so a configuration file discovered in a checked-out repository cannot cause any WebAssembly to run. It can only widen what a plugin you already asked for with --plugins is granted.

Installation

From source

# Default build (CLI + builtin plugins)
cargo install --path .

# With web server support
cargo install --path . --features web-server

# Build with embedded WASM plugins instead of native (requires WASM toolchain)
make build-plugins
cargo install --path . --no-default-features --features cli,wasm-builtin-plugins

Cargo features

Feature Description
cli Command-line interface (default); includes plugins
native-builtin-plugins Compile builtin plugins as native Rust (default)
wasm-builtin-plugins Embed builtin WASM plugins in the binary (requires make build-plugins)
plugins Loading external WASM plugins with --plugins; part of cli, on its own for library use
web-server Built-in web server for browser UI
wasm WebAssembly target support

License

MIT

The binary also contains third-party crates under their own licenses; nginx-lint license prints their notices (kept in licenses/THIRD-PARTY-NOTICES, regenerated with make build-licenses). nginx-lint-plugin-sdk license does the same for the plugin SDK binary and the Lua runtime it embeds in plugins.

Each GitHub release also carries a CycloneDX SBOM beside every archive (nginx-lint-<target>.cdx.json, nginx-lint-plugin-sdk-<target>.cdx.json). The one for nginx-lint over-reports: it also lists crates used only by the builtin plugins' tests (testcontainers and its dependencies), which the binary does not contain. The one for nginx-lint-plugin-sdk under-reports: it lists Rust crates only, so the C code it carries is missing — Lua, compiled into the binary through mlua, and the embedded Lua runtime (Lua, wasi-libc, LLVM compiler-rt) that also goes into every plugin it builds. nginx-lint-plugin-sdk license names those components.

About

A linter for nginx configuration files with WASM plugin support

Topics

Resources

Stars

57 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages