Nginx Administration and Internals

Last update: 2022-12-19 02:06:42 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

Nginx is an event-driven HTTP, stream, and mail proxy and server. Treat the configuration, its process privileges, and upstream timeouts as production code: validate changes before reloading and retain a known-good configuration.

Event-Driven I/O #

Nginx is built around nonblocking event loops. A master process reads the configuration and starts workers; each worker then waits on epoll, kqueue, or the platform’s equivalent readiness backend. The worker keeps a connection record for each peer and advances each request only when the socket becomes ready.

One event loop can therefore handle many connections without one thread per request. The model also explains why logging and buffering choices matter: the worker must stay responsive while upstreams, clients, and disk writes progress independently.

Isolated Configuration #

Run an instance with an explicit prefix and configuration when testing a local build. The prefix resolves relative paths such as logs and temporary files.

sh
nginx -p "$PWD" -c nginx.conf
nginx -t -p "$PWD" -c nginx.conf

For an unprivileged local process, use a non-privileged listener and set pid, logs, and each request-body or upstream temporary path to writable locations. daemon off; and master_process off; simplify foreground debugging but are not production settings.

The include directive is textual configuration inclusion relative to the prefix or an absolute path. Validate the complete include graph with nginx -t and inspect the expanded configuration with nginx -T. On reload, the master parses and validates the new configuration, starts new workers when successful, and asks old workers to finish current work before exiting. A parse or resource acquisition failure leaves the old configuration serving traffic.

Virtual Servers and CORS #

Nginx selects an HTTP virtual server from the listener and Host header. Within a listener, selection prefers an exact server_name, then the longest leading wildcard, then the longest trailing wildcard, and finally the first matching regular expression. server_name .example.com is shorthand for the apex and *.example.com; *.example.com does not match the apex alone.

After selecting a server, Nginx finds a location from prefix and regular expression rules. Exact locations win immediately; otherwise Nginx remembers the longest prefix and evaluates eligible regular expressions in order. The ^~ modifier prevents regular-expression checks after that prefix match. Internal rewrites can start another location search, so trace the final URI when proxy behavior differs from the visible request.

CORS must name permitted origins when credentials are allowed. * cannot be combined with Access-Control-Allow-Credentials: true. Reflect an origin only after an allow-list match and add Vary: Origin, because shared caches must not reuse a credentialed response for another origin.

nginx
map $http_origin $cors_origin {
    default "";
    "~^https://(app|admin)\.example\.com$" $http_origin;
}

server {
    location /api/ {
        if ($cors_origin != "") {
            add_header Access-Control-Allow-Origin $cors_origin always;
            add_header Access-Control-Allow-Credentials true always;
            add_header Vary Origin always;
        }
        proxy_pass http://api_upstream;
    }
}

Handle preflight OPTIONS requests with the same allow-list and explicit allowed methods and headers. Do not forward untrusted Host headers to an upstream that uses them for authorization or URL generation; set Host to the expected upstream name where required.

Proxied GET Requests #

HTTP defines GET as safe, but an application can still make it expensive. Content in a GET request has no generally defined semantics, so an upstream should not depend on a GET body unless a private contract defines and protects that behavior. Nginx can retry a proxied request after an upstream failure according to proxy_next_upstream; a retry can therefore cause the upstream application to execute the same GET more than once. Keep GET handlers free of side effects, set an explicit retry policy, and use an idempotency key or a different method for operations that create work.

An upstream response timeout does not prove that the upstream did nothing. Log request IDs at the proxy and application boundary, then use those IDs to distinguish a retry from a client retry or a duplicated message.

By default, proxying preserves the request method and body while constructing a new upstream request with Nginx-controlled headers. Set authority and forwarding metadata explicitly:

nginx
location /api/ {
    proxy_set_header Host api.internal.example;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://api_upstream/;
}

Because proxy_pass includes a URI here, the normalized part matching /api/ is replaced with / before forwarding. Without a URI in proxy_pass, Nginx forwards the request URI according to the documented rewrite rules. Review this distinction when changing a trailing slash. Do not convert a mutating request to GET with proxy_method unless the upstream contract also handles or discards the original request body deliberately.

Source Architecture #

Nginx starts a master process that reads configuration and manages workers. Workers use an event mechanism such as epoll or kqueue, maintain connections in ngx_connection_t, and advance request processing through handler phases. Modules declare a type—core, event, HTTP, stream, or mail—and register directives, configuration creation and merge functions, and runtime handlers.

Each module exposes a global ngx_module_t descriptor. The module type tells Nginx which configuration structures and initialization hooks belong together. Core source files and extra modules are wired up during the build, while a small set of modules can be built dynamically and loaded later.

HTTP request processing is organized into phases such as rewrite, access, content, and log. A request moves through those phases after parsing the request line and headers, thus a module can hook one stage without owning the entire request path.

Logging Internals #

ngx_log_error_core() is the core logging function. Higher-level macros such as ngx_log_error() and ngx_log_debug() wrap it and respect the configured log level. Debug logging is compiled and enabled only when the build and runtime configuration allow it.

The error_log directive controls where errors go. Access logs are separate and are intentionally not treated as the same channel as error logging. In debug builds, the debug log level makes it practical to trace a request through configuration parsing, location lookup, phase handlers, and upstream processing.

Build a debug binary from a pinned source release with --with-debug, run it with a dedicated prefix, and inspect error logs at the debug level. Start source reading with configuration parsing and cycle initialization, then follow an accepted connection through the event loop, HTTP request-line parsing, location configuration, phase handlers, and upstream processing.

ACdlmiiennitstfrlaotwosrswiogrnkaelrsc,onrfeilgoaduwpmosartskrteeerarmsworker

References #

Comments

Read or comment on GitHub (sign-in required to comment)