If you've browsed the web today, you've almost certainly touched Nginx without knowing it. It serves static files, proxies traffic to application servers, balances load across fleets of machines, terminates TLS, and caches content, often all at once. This guide covers what Nginx is, how it works internally, how to configure it, and how to run it well in production.
What Is Nginx?
Nginx (pronounced "engine-x") is an open-source web server that is also used as a reverse proxy, load balancer, HTTP cache, and mail proxy. It is known for high performance, low memory usage, and stability under heavy concurrent load.
In practice, Nginx usually plays one or more of these roles:
- Web server: serves HTML, CSS, JavaScript, images, and other static files directly from disk.
- Reverse proxy: sits in front of backend applications (Node.js, Python, PHP, Java, Go) and forwards client requests to them.
- Load balancer: distributes incoming requests across several backend servers.
- HTTP cache: stores backend responses so repeated requests are answered instantly.
- TLS terminator: handles HTTPS encryption so backends can speak plain HTTP internally.
- API gateway (lightweight): routes, rate-limits, and authenticates API traffic.
A Brief History
Nginx was created by Igor Sysoev, a Russian engineer, who began work in 2002 and released it publicly in 2004. His goal was to solve the "C10K problem": how to handle ten thousand concurrent connections on a single server. Traditional servers like Apache, which used a process or thread per connection, struggled at that scale.
Nginx's answer was an event-driven architecture. The company Nginx, Inc. was founded in 2011 and later acquired by F5 Networks in 2019. Today there are two main flavors: the open-source Nginx and the commercial Nginx Plus, which adds features like advanced monitoring, active health checks, and a dynamic API. A popular fork, Freenginx, also exists, and the broader ecosystem includes OpenResty, which embeds Lua scripting into Nginx.
How Nginx Works: The Architecture
Understanding Nginx's architecture explains why it's so efficient.
Master and Worker Processes
When Nginx starts, it launches one master process and several worker processes.
- The master process reads configuration, binds to ports, and manages the workers. It doesn't handle client traffic.
- Worker processes do the real work of accepting connections and processing requests.
A common setting is one worker per CPU core, which you can set with worker_processes auto;.
Event-Driven, Non-Blocking I/O
Each worker runs an event loop that handles thousands of connections simultaneously. Instead of dedicating a thread to each connection and waiting while it reads or writes, a worker uses operating system mechanisms like epoll (Linux) or kqueue (BSD/macOS) to be notified when a connection is ready. When one connection is waiting on the network, the worker moves on to another. Because there's no per-connection thread overhead, memory use stays small and predictable even with tens of thousands of connections.
Modular Design
Nginx is built from modules for HTTP, proxying, gzip, SSL, rewriting, caching, and more. Some modules are compiled in, while others are loaded dynamically with the load_module directive. This keeps the core lean and lets you add only what you need.
Installing Nginx
On Debian or Ubuntu:
sudo apt update
sudo apt install nginx
On RHEL, CentOS, Rocky, or Fedora:
sudo dnf install nginx
On macOS with Homebrew:
brew install nginx
Or run it in Docker:
docker run -d -p 80:80 nginx
After installing, start and enable it:
sudo systemctl start nginx
sudo systemctl enable nginx
Visit your server's IP address and you should see the default welcome page. Useful commands to know:
sudo nginx -t # test configuration syntax
sudo nginx -s reload # reload config without downtime
sudo systemctl status nginx
Always run nginx -t before reloading. A syntax error caught here saves you from an outage.
Understanding the Configuration File
Nginx is configured through plain-text files, typically /etc/nginx/nginx.conf, plus included files in /etc/nginx/conf.d/ or /etc/nginx/sites-enabled/.
The configuration uses directives organized into contexts (blocks):
user www-data;
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
index index.html;
}
}
}
The main contexts are:
- main: global settings like the user and worker count.
- events: connection-processing settings.
- http: everything related to HTTP traffic.
- server: defines a virtual host (a site).
- location: defines how to handle requests matching a URI pattern.
- upstream: defines a group of backend servers.
Directives inherit downward, so settings in http apply to all server blocks unless overridden. Every simple directive ends with a semicolon, and forgetting one is the most common beginner error.
Server Blocks and Location Matching
Server Blocks
Server blocks let one Nginx instance host many sites. Nginx picks the block by matching the request's Host header against server_name:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example;
}
server {
listen 80;
server_name blog.example.com;
root /var/www/blog;
}
Location Matching
Location blocks decide how to handle specific URIs. Nginx evaluates them using this priority:
location = /pathis an exact match and wins immediately.location ^~ /pathis a prefix match that stops regex checking.location ~ regexis a case-sensitive regex, and~*is case-insensitive.location /pathis a plain prefix match, with the longest one winning.
Example:
location = /health {
return 200 "ok";
}
location ^~ /static/ {
root /var/www;
}
location ~* \.(jpg|jpeg|png|gif|css|js)$ {
expires 30d;
}
Getting this order wrong causes many "why is my request going to the wrong place" bugs, so it's worth memorizing.
Serving Static Content
Serving static files is where Nginx shines:
server {
listen 80;
server_name example.com;
root /var/www/example;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
try_files checks whether the requested file exists, then a directory, and returns a 404 if neither does. For single-page applications like React or Vue, you typically fall back to index.html:
location / {
try_files $uri /index.html;
}
Nginx as a Reverse Proxy
A reverse proxy accepts client requests and forwards them to another server. This is the most common production use of Nginx. Instead of exposing your Node.js or Django app directly, you place Nginx in front:
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
The proxy_set_header lines matter because, without them, your backend sees every request as coming from Nginx itself. These headers pass along the original host, client IP, and protocol.
Why use a reverse proxy at all?
- Security: backends aren't exposed directly to the internet.
- Performance: Nginx buffers slow clients so your app isn't tied up waiting on them.
- Flexibility: you can add caching, compression, TLS, and rate limiting without touching application code.
- Consolidation: many apps can share one public IP and port.
WebSocket Support
WebSockets need extra headers to upgrade the connection:
location /ws/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
FastCGI and PHP
For PHP, Nginx talks to PHP-FPM using FastCGI rather than proxy_pass:
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
Similar directives exist for uWSGI (uwsgi_pass) and SCGI.
Load Balancing
When one backend isn't enough, define an upstream group:
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
By default Nginx uses round-robin. Other methods include:
- least_conn: sends the request to the server with the fewest active connections.
- ip_hash: the same client IP always reaches the same server, which is useful for simple session stickiness.
- hash: custom key-based hashing, such as on a URI or cookie.
- random: picks randomly, optionally with a "two choices" refinement.
You can also assign weights and mark servers as backups:
upstream backend {
least_conn;
server 10.0.0.1:8080 weight=3;
server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.3:8080 backup;
}
Open-source Nginx performs passive health checks: if a server fails repeatedly, it's temporarily removed from rotation. Active health checks, which probe servers on a schedule, are an Nginx Plus feature.
HTTPS and TLS
Modern sites must use HTTPS. Nginx handles it efficiently:
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
location / {
root /var/www/example;
}
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
The second block redirects all HTTP traffic to HTTPS.
Let's Encrypt provides free certificates, and Certbot automates issuing and renewing them:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Some practical TLS tips:
- Disable old protocols (SSLv3, TLS 1.0, and TLS 1.1).
- Enable HTTP/2, and consider HTTP/3 (QUIC), which newer Nginx versions support.
- Use OCSP stapling to speed up certificate validation.
- Add an HSTS header so browsers always use HTTPS.
- Test your setup with tools like SSL Labs.
Caching
Nginx can cache responses from backends, which can dramatically reduce load.
Proxy Cache
http {
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mycache:10m
max_size=1g inactive=60m use_temp_path=off;
server {
location / {
proxy_cache mycache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
}
}
}
The X-Cache-Status header (HIT, MISS, EXPIRED, and so on) makes it easy to see whether caching works. proxy_cache_use_stale is valuable because it serves old content if the backend is down.
Browser Caching
For static assets, tell browsers to keep them:
location ~* \.(css|js|jpg|png|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Be careful not to cache personalized or authenticated responses. Use proxy_cache_bypass and proxy_no_cache to exclude requests that carry cookies or authorization headers.
Performance Tuning
A few well-chosen settings give big gains.
Worker settings:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
multi_accept on;
}
Max concurrent clients is roughly worker_processes × worker_connections, and each proxied request uses two connections (client and upstream).
Compression:
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript
application/xml image/svg+xml;
Brotli compression, available through a module, compresses better than gzip for text assets.
Efficient file sending:
sendfile on;
tcp_nopush on;
tcp_nodelay on;
sendfile lets the kernel transfer files directly, skipping a copy through user space.
Keepalive to upstreams: reusing connections to backends avoids repeated TCP handshakes:
upstream backend {
server 127.0.0.1:3000;
keepalive 32;
}
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
Also consider open_file_cache to cache file descriptors and metadata for frequently served files.
Security Hardening
Nginx is secure by default, but a public server deserves extra care.
Hide version information:
server_tokens off;
Add security headers:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
A Content-Security-Policy header is also worth adding once you've tested it against your site.
Limit request sizes and timeouts to blunt abuse:
client_max_body_size 10m;
client_body_timeout 12s;
client_header_timeout 12s;
send_timeout 10s;
Block access to sensitive files:
location ~ /\.(?!well-known) {
deny all;
}
Restrict by IP and add authentication:
location /admin/ {
allow 203.0.113.0/24;
deny all;
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
}
Beyond configuration, run Nginx workers as an unprivileged user, keep the software patched, and consider a web application firewall such as ModSecurity or a managed WAF in front.
Rate Limiting
Rate limiting protects against brute-force attempts, scrapers, and overload.
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_conn conn 10;
proxy_pass http://backend;
}
}
}
rate sets the sustained limit, while burst allows short spikes. Requests beyond the limit receive a 503 by default, which you can change with limit_req_status 429;.
Redirects and Rewrites
Simple redirects use return, which is faster and clearer than rewrite:
return 301 https://example.com$request_uri;
For pattern-based changes, use rewrite:
rewrite ^/old-blog/(.*)$ /blog/$1 permanent;
You can also redirect www to the bare domain, serve custom error pages with error_page 404 /404.html;, and use the map directive to build lookup tables for things like redirect maps or conditional logic. Avoid using if inside location blocks where possible, as it behaves in surprising ways. The Nginx community even has a well-known article titled "If Is Evil."
Logging and Monitoring
Nginx writes two logs by default:
- Access log (
/var/log/nginx/access.log) records every request. - Error log (
/var/log/nginx/error.log) records problems.
You can customize the format:
log_format main '$remote_addr - $request [$status] '
'$body_bytes_sent "$http_referer" '
'rt=$request_time urt=$upstream_response_time';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
Including $request_time and $upstream_response_time helps you tell whether slowness comes from Nginx or the backend. For structured pipelines, log in JSON and ship the output to a stack like ELK, Loki, or Datadog.
For metrics, the built-in stub_status module exposes basic connection counts, and exporters like nginx-prometheus-exporter feed Prometheus and Grafana dashboards. Rotate logs with logrotate so they don't fill your disk.
Nginx vs Apache
Both are excellent, mature servers, and the right choice depends on your needs.
| Aspect | Nginx | Apache |
|---|---|---|
| Architecture | Event-driven, asynchronous | Process/thread-based (with event MPM option) |
| Static content | Very fast | Good |
| Memory under load | Low | Higher |
| Configuration | Central config files | Supports per-directory .htaccess |
| Dynamic content | Via proxying (PHP-FPM, etc.) | Built-in modules like mod_php |
| Flexibility | Great as proxy and load balancer | Great for shared hosting |
Many teams combine them, using Nginx at the edge for TLS, caching, and static files, with Apache behind it for legacy applications. Note that Nginx has no .htaccess equivalent, which is part of why it's faster but requires you to put rules in central config.
Nginx in Modern Infrastructure
Nginx has adapted well to the container and cloud era.
Docker: the official image is lightweight, and you can mount your own config or bake it into a custom image.
Kubernetes Ingress: the NGINX Ingress Controller is one of the most widely used ways to route external traffic into a cluster. Note that the community ingress-nginx project has been moving toward retirement in favor of the newer Gateway API, so check the current status before choosing it for a new deployment.
API gateway: with rate limiting, JWT validation (in Plus or via modules), and routing rules, Nginx can serve as a simple gateway.
TCP/UDP proxying: the stream module proxies non-HTTP traffic such as databases, DNS, or MQTT:
stream {
server {
listen 5432;
proxy_pass 10.0.0.5:5432;
}
}
Nginx Plus and alternatives: if you need dashboards, active health checks, or dynamic reconfiguration, Nginx Plus provides them commercially. Alternatives like Caddy (automatic HTTPS), HAProxy (specialized load balancing), and Envoy (service meshes) are worth knowing about, and each has strengths where Nginx is not the best fit.
Troubleshooting Common Problems
502 Bad Gateway: Nginx couldn't get a valid response from the backend. Check that the app is running, the port or socket path is right, and permissions on the socket allow Nginx to connect. On SELinux systems, you may need setsebool -P httpd_can_network_connect 1.
504 Gateway Timeout: the backend took too long. Increase proxy_read_timeout if the work is legitimately slow, or investigate the backend.
403 Forbidden: usually file permissions or a missing index file. Make sure the Nginx user can read the files and traverse every parent directory.
404 on a valid page: check the root versus alias directive. root appends the URI to the path, while alias replaces the matched location.
413 Request Entity Too Large: raise client_max_body_size.
Config changes not applying: you need to run nginx -s reload, and you should confirm you edited the file Nginx actually loads. nginx -T prints the full effective configuration, which is invaluable for debugging.
A good habit: test with nginx -t, reload, then check the error log with tail -f /var/log/nginx/error.log.
Best Practices Checklist
- Keep configuration modular, with one file per site and shared snippets via
include. - Store configs in version control.
- Always run
nginx -tbefore reloading. - Redirect HTTP to HTTPS and automate certificate renewal.
- Turn off
server_tokensand add security headers. - Enable gzip or Brotli, and set sensible cache headers.
- Use rate limiting on login and API endpoints.
- Log request timing, and monitor error rates and latency.
- Keep Nginx updated for security fixes.
- Prefer
returnoverrewrite, and avoidifin locations.
Conclusion
Nginx earned its place at the center of the modern web through a simple idea executed well: handle huge numbers of connections efficiently using an event-driven design. From there it grew into a Swiss Army knife for web infrastructure, covering static hosting, reverse proxying, load balancing, TLS termination, caching, and more.
You don't need to master everything at once. Start by serving a static site, then put it in front of an application as a reverse proxy, add HTTPS with Let's Encrypt, and layer in caching, compression, and rate limiting as your traffic grows. Each step builds on the same simple configuration language, and each delivers a real, measurable improvement.
Whether you're running a personal blog or a platform serving millions of users, understanding Nginx deeply is one of the highest-leverage skills in web operations. Experiment on a test server, read the official documentation at nginx.org, and keep nginx -t close at hand.

Comments
Post a Comment