openskills.info
NGINX Fundamentals logo

NGINX Fundamentals

itWeb servers, proxies, and traffic management

NGINX Fundamentals

NGINX is a web server, reverse proxy, and load balancer. It accepts HTTP (and other protocol) connections, decides what to do with each request based on its configuration, and either serves a file directly or hands the request to another process. It runs some of the busiest sites on the internet because of how it handles concurrency: one small set of worker processes serves thousands of simultaneous connections without spawning a thread or process per connection.

The useful mental model is a request router with a rulebook. The rulebook is the configuration file. Every incoming request gets matched against that rulebook — first to a virtual server, then to a location inside it — and the matched rule decides what happens next: serve a file, proxy to a backend, redirect, or reject.

Why NGINX exists

Early web servers like Apache HTTP Server handled each connection with a dedicated process or thread. That model is simple to reason about, but it does not scale gracefully when a server needs to hold open thousands of slow or idle connections — the C10K problem. NGINX was built around an event-driven, asynchronous architecture instead: a fixed number of worker processes each run an event loop and multiplex many connections without blocking on any single one.

That architecture makes NGINX a natural fit for jobs that involve high connection counts or connection reuse, not just raw CPU-bound page rendering:

  • serving static files (HTML, images, CSS, JavaScript) at high concurrency;
  • terminating TLS in front of application servers;
  • reverse-proxying requests to application backends (PHP-FPM, Node.js, Python WSGI/ASGI apps, Java application servers);
  • load balancing across multiple backend instances;
  • acting as an API gateway or edge cache.

Master and worker processes

An NGINX instance is a master process plus one or more worker processes.

  • The master process reads and validates the configuration file, opens listening sockets, and manages workers — starting, stopping, and restarting them as needed. It does not handle client requests itself.
  • Worker processes do the actual work: accepting connections, reading requests, running the configured logic, and talking to upstream servers. Each worker runs an event loop and can hold many connections open at once.

You control the number of workers with the worker_processes directive in the main configuration context; the default is 1, and auto tells NGINX to detect the number of available CPU cores and match it. Within the events block, worker_connections caps how many simultaneous connections one worker can hold open — the practical connection ceiling for the whole instance is roughly worker_processes times worker_connections, bounded by the operating system's open-file limit (worker_rlimit_nofile).

This separation is also what lets configuration reloads and binary upgrades happen without clients noticing a dropped connection: the master process can start new workers with a freshly validated configuration and let old workers finish in-flight requests before exiting, without ever closing the listening socket.

Configuration structure: contexts and directives

NGINX configuration is a tree of nested blocks called contexts, each holding directives. The two directive forms are:

  • a simple directive — a name, one or more parameters, and a terminating semicolon: worker_processes auto;
  • a block directive — a name, optional parameters, and a body in braces: server { ... }

The standard context nesting for HTTP traffic is:

main
 └─ events { }
 └─ http {
      └─ server { }
           └─ location { }
    }
  • main — top-level settings that apply to the whole instance (worker counts, user, error log path).
  • events — connection-processing settings, chiefly worker_connections.
  • http — settings shared by all HTTP virtual servers (logging formats, MIME types, default timeouts, upstream definitions).
  • server — one virtual server, matched by listening address/port and then by the Host header. A configuration file typically has several server blocks, one per site or API.
  • location — a rule matched against the request URI inside a server. This is where most day-to-day configuration lives.

Directives set in an outer context are inherited by inner contexts unless the inner context overrides them, so shared settings belong at the highest context that still makes sense.

Virtual servers: how NGINX picks a server block

For every connection, NGINX picks a server block in two stages:

  1. Address and port — the listen directive on each server block is matched against the socket the connection arrived on. If listen is omitted entirely, NGINX defaults to *:80 (or *:8000 when not running with superuser privileges).
  2. Host header — among the server blocks that matched the address and port, NGINX compares the request's Host header against each block's server_name values, checking in this order: exact name, then the longest wildcard starting with * (*.example.com), then the longest wildcard ending with * (www.*), then the first matching regular expression (prefixed with ~) in configuration order.

If the Host header does not match any server_name, or the request has no Host header at all, NGINX falls back to the default server for that address/port — the server block marked with the default_server parameter on listen, or the first server block for that address/port if none is marked. The default server is a property of the listening address and port, not of any particular name, so different ports can have different default servers.

Locations: how NGINX picks a rule inside a server

Inside a matched server block, NGINX chooses a location by comparing the request URI against each location's pattern. The modifier in front of the pattern changes how the comparison works:

ModifierMeaningMatching behavior
=exact matchstops the search immediately if it matches
(none)prefix matchthe longest matching prefix is remembered
^~prefix match, priorityif this is the longest prefix match, regex locations are skipped
~regular expression, case-sensitivechecked in configuration order
~*regular expression, case-insensitivechecked in configuration order
@named locationonly reachable through internal redirects (try_files, error_page); never matched directly against a request

The overall algorithm: NGINX first checks for an exact (=) match and stops there if found. Otherwise it walks all prefix locations and remembers the longest one that matches. If that longest prefix location was declared with ^~, the search stops there. Otherwise NGINX checks the regular-expression locations in the order they appear in the file and uses the first one that matches. If none match, it falls back to the remembered prefix location.

This is the single most common source of "why isn't my config doing what I expect" confusion — a location /images/ { } prefix block can be silently overridden by an earlier location ~ \.(gif|jpg|png)$ { } regex block for the same request, because regex locations, when they match, win over a plain prefix match.

Serving files: root, alias, and try_files

For static content, root and alias both map a URI to a filesystem path, but they combine that path differently:

  • root appends the full request URI to the given path. location /images/ { root /data; } maps a request for /images/cat.png to /data/images/cat.png.
  • alias replaces the matched location prefix with the given path. location /images/ { alias /data/pics/; } maps that same request to /data/pics/cat.png — the /images/ prefix is dropped, not appended.

Use root when the URI structure mirrors the filesystem structure under one directory; use alias when the URI name and the directory name diverge.

try_files checks a list of candidate paths in order and serves the first one that exists, falling back to a final URI (often an internal named location) or an explicit status code if none exist. It is the standard way to support both real static files and an application front controller in the same location: try_files $uri $uri/ @app; serves an existing file or directory as-is and routes everything else to a named location that hands off to the application.

Reverse proxying, in one paragraph

proxy_pass turns a location into a reverse proxy: instead of serving a file, NGINX forwards the request to another HTTP server (an application server, a container, another NGINX instance) and relays the response back to the client. This is the foundation of using NGINX in front of application backends — for TLS termination, for shielding backend processes from direct exposure, and for routing different URL prefixes to different services. This course covers the concept only far enough to place it in the bigger picture; upstream groups, load-balancing algorithms, health checks, and header handling belong to the follow-on NGINX Reverse Proxy and Load Balancing course.

TLS termination, in one paragraph

An HTTPS server block adds listen 443 ssl;, a certificate (ssl_certificate), and its private key (ssl_certificate_key). NGINX handles the TLS handshake and decrypts traffic before it reaches any location logic, so backends behind it can speak plain HTTP. Because handshakes are CPU-intensive, production configurations typically also enable keepalive connections and a shared SSL session cache (ssl_session_cache shared:SSL:10m;) so repeat clients can resume a session instead of renegotiating from scratch. Hardening those settings — protocol and cipher selection, HSTS, OCSP stapling — belongs to the follow-on NGINX Security Hardening course.

Starting, stopping, and reloading

NGINX is controlled either by sending it a signal directly or through its own -s flag, which translates a few named signals for you:

nginx                 # start
nginx -s stop         # fast shutdown
nginx -s quit         # graceful shutdown: finish in-flight requests, then exit
nginx -s reload       # re-read configuration
nginx -s reopen       # reopen log files (e.g. after log rotation)

reload is the everyday operational command. The master process checks the syntax of the new configuration first; if it is invalid, NGINX logs an error and keeps running on the old configuration unchanged. If it is valid, the master starts new worker processes with the new configuration and signals old workers to shut down gracefully once their current requests finish. Clients never see a dropped connection or a gap in service from a configuration change alone — that graceful handoff is a direct consequence of the master/worker split described above.

What NGINX is not

  • It is not an application server. It does not execute PHP, Python, Java, or Node.js code itself; it proxies to processes that do (PHP-FPM, uWSGI, a Node.js process, and so on).
  • It is not a database or a cache backend, though it can front one and cache responses at the HTTP layer.
  • It is not automatically a load balancer with health-aware failover out of the box — passive checks (max_fails, fail_timeout) exist, but active health checking and richer traffic-shaping belong to NGINX Plus or paired tooling.
  • It is not a substitute for application-level input validation and authorization, even when it also does request filtering and TLS termination.

Where NGINX sits in the landscape

NGINX is one of several servers built for the same event-driven niche — Apache HTTP Server predates it and uses a heavier process/thread model per connection by default; HAProxy and Envoy specialize more narrowly in proxying and load balancing; Caddy and Traefik emphasize automatic TLS and container-native configuration. NGINX's particular strength is being a stable, low-overhead default that handles static files, TLS termination, and reverse proxying equally well from one configuration language, which is why it shows up at the edge of so many different stacks.

A practical path through this material

  1. Install NGINX and locate its configuration file and log directory for your platform.
  2. Read the shipped default nginx.conf end to end and identify the main, events, http, server, and location contexts in it.
  3. Serve a directory of static files from a location block and confirm the root-based path mapping.
  4. Add a second server block on the same port with a different server_name and confirm host-based routing with curl -H "Host: ...".
  5. Add a location with proxy_pass in front of any small backend you have available, and watch requests flow through in access.log.
  6. Practice nginx -s reload after an intentional syntax error, and confirm the old configuration keeps serving traffic.
  7. Move on to the NGINX Reverse Proxy and Load Balancing course for upstream groups and balancing algorithms, and to NGINX Security Hardening for TLS and access-control hardening.