NGINX (pronounced "engine-x") is a high-performance, open-source web server, reverse proxy, load balancer, and HTTP cache.
It was built to solve the C10K problem — handling 10,000+ simultaneous connections on a single server. Unlike Apache (which spawns a new thread per connection), NGINX uses an event-driven, non-blocking architecture, so a single worker process can handle thousands of connections at once with minimal memory.
Traditional Server (Apache) NGINX Event-Driven Model
───────────────────────────── ─────────────────────────────
Request 1 → Thread 1 (blocked) Single Worker
Request 2 → Thread 2 (blocked) ├── Handle Req 1 → async I/O
Request 3 → Thread 3 (blocked) ├── Handle Req 2 → async I/O
...10,000 req = 10,000 threads ├── Handle Req 3 → async I/O
RAM exhausted └── 10,000 connections ✓
Why Do We Need NGINX?
Running a Node.js or any backend app directly on port 80/443 is not safe or scalable. Here is what NGINX solves:
| Problem | Without NGINX | With NGINX |
|---|---|---|
| HTTPS/SSL | Complex in app code | NGINX handles it |
| Multiple apps on one server | Port conflicts | Route by domain/path |
| Static file serving | Node.js serves every file | NGINX serves directly (10x faster) |
| Load balancing | Single instance | Distribute across many |
| Rate limiting | App-level logic | Built into NGINX |
| Security headers | Manual in app | One place in NGINX config |
| Crash recovery | App dies, site is down | NGINX buffers while app restarts |
Real-world flow:
Browser
│
▼
NGINX :443 ──── serves static files directly
│
└──► Node.js :3000 (only handles API / app logic)
Installation on Linux
Ubuntu / Debian
# Update packages
sudo apt update
# Install NGINX
sudo apt install nginx -y
# Start the service
sudo systemctl start nginx
# Enable auto-start on boot
sudo systemctl enable nginx
# Allow HTTP and HTTPS through firewall
sudo ufw allow "Nginx Full"
# Verify — open http://your-server-ip in browser
Amazon Linux 2 (EC2)
sudo yum update -y
sudo amazon-linux-extras install nginx1 -y
sudo systemctl start nginx
sudo systemctl enable nginx
Amazon Linux 2023
sudo dnf install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
CentOS / RHEL
sudo yum install epel-release -y
sudo yum install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
Essential Commands
# ── Service Control ──────────────────────────────────────
sudo systemctl start nginx # Start
sudo systemctl stop nginx # Stop
sudo systemctl restart nginx # Restart (brief downtime)
sudo systemctl reload nginx # Reload config (zero downtime) ✓ preferred
sudo systemctl status nginx # Check if running
sudo systemctl enable nginx # Auto-start on boot
# ── Config Testing ───────────────────────────────────────
sudo nginx -t # Test config for errors (always run before reload)
sudo nginx -T # Test + print full resulting config
# ── Safe Reload Pattern (use this every time) ────────────
sudo nginx -t && sudo systemctl reload nginx
# ── Logs ─────────────────────────────────────────────────
sudo tail -f /var/log/nginx/access.log # Live access log
sudo tail -f /var/log/nginx/error.log # Live error log
# ── Info ─────────────────────────────────────────────────
nginx -v # NGINX version
nginx -V # Version + build flags + modules
ps aux | grep nginx # See master and worker processes
Rule of thumb: Always run
sudo nginx -tbefore reloading. A config error will crash NGINX if you restart without testing.
File Structure
/etc/nginx/
├── nginx.conf ← Main config file (global settings)
├── conf.d/ ← Drop-in configs (auto-loaded, *.conf)
│ └── default.conf
├── sites-available/ ← All virtual host configs (inactive)
│ ├── myapp.com
│ └── api.myapp.com
├── sites-enabled/ ← Symlinks to active configs
│ └── myapp.com → ../sites-available/myapp.com
├── snippets/ ← Reusable config fragments
│ └── ssl-params.conf
├── mime.types ← File extension → Content-Type mapping
├── fastcgi_params ← FastCGI defaults (PHP)
└── proxy_params ← Proxy header defaults
/var/log/nginx/
├── access.log ← Every request logged here
└── error.log ← Errors and warnings
/var/www/html/ ← Default web root
sites-available vs sites-enabled
# Enable a site (create symlink)
sudo ln -s /etc/nginx/sites-available/myapp.com /etc/nginx/sites-enabled/
# Disable a site (remove symlink — source file stays safe)
sudo rm /etc/nginx/sites-enabled/myapp.com
# Reload to apply
sudo nginx -t && sudo systemctl reload nginx
nginx.conf — Core Structure
user www-data;
worker_processes auto; # One per CPU core
events {
worker_connections 1024; # Max connections per worker
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
# Load all configs from these directories
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Basic Configuration Guide
Every NGINX config is built from two core blocks:
server block
Defines how to handle a domain or IP + port.
server {
listen 80; # Port
server_name example.com www.example.com; # Domain(s)
root /var/www/example; # Files directory
index index.html;
# ... location blocks go here
}
location block
Matches URL paths and defines what to do.
# Exact match — only /health
location = /health {
return 200 "OK";
}
# Prefix match — /api and anything starting with /api
location /api/ {
proxy_pass http://localhost:3000;
}
# Regex match — .jpg .png .css .js files
location ~* \.(jpg|png|css|js|ico|woff2)$ {
expires 30d;
}
# Catch-all — everything else
location / {
try_files $uri $uri/ /index.html;
}
Location Priority (highest to lowest)
| Priority | Syntax | Behavior |
|---|---|---|
| 1st | = /exact |
Exact match, stops immediately |
| 2nd | ^~ /prefix |
Best prefix, skips regex check |
| 3rd | ~ pattern |
Regex (case-sensitive) |
| 3rd | ~* pattern |
Regex (case-insensitive) |
| 4th | /prefix |
General prefix match |
React App Configuration
A React app builds to static HTML/CSS/JS files. NGINX serves them directly — no Node.js needed for the frontend.
# Build your React app first
npm run build
# This creates a /dist or /build folder
# Copy to web root
sudo mkdir -p /var/www/myapp
sudo cp -r dist/* /var/www/myapp/
sudo chown -R www-data:www-data /var/www/myapp
# /etc/nginx/sites-available/myapp.com
server {
listen 80;
server_name myapp.com www.myapp.com;
root /var/www/myapp;
index index.html;
# Gzip compression
gzip on;
gzip_types text/html text/css application/javascript application/json;
# React Router — all routes serve index.html (SPA fallback)
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets aggressively
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2|woff|ttf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Never cache the HTML entry point
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
Why
try_files $uri $uri/ /index.html?
React Router handles routing client-side. When someone refreshes/dashboard, the browser asks NGINX for that file — which doesn't exist on disk. The fallback toindex.htmllets React Router take over.
Node.js App Configuration
Node.js runs on a local port. NGINX sits in front and proxies traffic to it.
# /etc/nginx/sites-available/api.myapp.com
server {
listen 80;
server_name api.myapp.com;
# Increase upload size limit if needed
client_max_body_size 50M;
location / {
proxy_pass http://localhost:3000; # Your Node.js port
# Required headers
proxy_http_version 1.1;
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;
# WebSocket support (Socket.io, ws)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
In your Express.js app, add this so req.ip returns the real client IP:
app.set('trust proxy', 1);
Why These Headers Matter
| Header | Value | Purpose |
|---|---|---|
Host |
$host |
Backend sees correct domain, not localhost
|
X-Real-IP |
$remote_addr |
Backend gets real client IP |
X-Forwarded-For |
$proxy_add_x_forwarded_for |
Full IP chain through proxies |
X-Forwarded-Proto |
$scheme |
Backend knows if client used http or https
|
SSL / HTTPS Setup
Option 1 — Let's Encrypt (Free, Recommended)
# Install Certbot
sudo apt install certbot python3-certbot-nginx -y
# Get certificate — Certbot auto-edits your NGINX config
sudo certbot --nginx -d myapp.com -d www.myapp.com
# Test auto-renewal
sudo certbot renew --dry-run
Certbot adds SSL to your config automatically. Your site is now on HTTPS.
Option 2 — Manual SSL Configuration
# /etc/nginx/sites-available/myapp.com
# Redirect all HTTP → HTTPS
server {
listen 80;
server_name myapp.com www.myapp.com;
return 301 https://$host$request_uri;
}
# Main HTTPS server
server {
listen 443 ssl http2;
server_name myapp.com www.myapp.com;
# Certificate files
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
# Modern TLS only
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# SSL session cache (performance)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# HSTS — tell browsers to always use HTTPS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
location / {
proxy_pass http://localhost: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 https;
}
}
Use
listen 443 ssl http2— HTTP/2 allows multiplexing (multiple requests over one connection), which noticeably improves performance for sites with many assets.
Reverse Proxy
A reverse proxy sits on the server side. The client sends requests to the proxy, and the proxy decides which backend server handles it. The client has no idea which backend it's talking to.
Client → [NGINX Reverse Proxy] → Backend Server 1
│
└──────────► Backend Server 2
│
└──────────► Backend Server 3
The client only ever talks to NGINX.
It has no idea those backend servers even exist.
The word "reverse" simply means — instead of the proxy being on the client side, it's on the server side.
Client ──► NGINX :443 ──► Node.js :3000
(public) (localhost only, never exposed)
server {
listen 80;
server_name myapp.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
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;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Serve static files directly — bypass Node.js entirely
location /static/ {
alias /var/www/myapp/public/;
expires 30d;
access_log off;
}
}
Proxy to a Different Path
# Request: GET /api/users
# Backend receives: GET /users (strips the /api prefix)
location /api/ {
proxy_pass http://localhost:3000/; # trailing slash strips /api/
}
# Request: GET /api/users
# Backend receives: GET /api/users (full path preserved)
location /api/ {
proxy_pass http://localhost:3000; # no trailing slash
}
Upstream & Load Balancing
upstream block
The upstream block defines a named group of backend servers. You reference it in proxy_pass.
upstream my_backend {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
server {
listen 80;
server_name myapp.com;
location / {
proxy_pass http://my_backend; # ← reference by name
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Load Balancing Algorithms
Round Robin (default) — requests distributed evenly in sequence
upstream backend {
server 10.0.0.1:3000;
server 10.0.0.2:3000;
server 10.0.0.3:3000;
}
Weighted Round Robin — one server gets more traffic
upstream backend {
server 10.0.0.1:3000 weight=3; # gets 3x more requests
server 10.0.0.2:3000 weight=1;
}
Least Connections — new request goes to server with fewest active connections
upstream backend {
least_conn;
server 10.0.0.1:3000;
server 10.0.0.2:3000;
}
IP Hash — same client IP always goes to the same server (sticky sessions)
upstream backend {
ip_hash;
server 10.0.0.1:3000;
server 10.0.0.2:3000;
}
Failover Configuration
upstream backend {
server 10.0.0.1:3000 weight=1 max_fails=3 fail_timeout=30s;
server 10.0.0.2:3000 weight=1 max_fails=3 fail_timeout=30s;
# Backup — only used when all primary servers are down
server 10.0.0.3:3000 backup;
# Keep connections to backend warm (performance)
keepalive 32;
}
| Parameter | Meaning |
|---|---|
weight=3 |
Gets 3× more traffic than weight=1
|
max_fails=3 |
Mark as down after 3 consecutive failures |
fail_timeout=30s |
How long to mark it as down before retrying |
backup |
Only used when all other servers are unavailable |
down |
Permanently offline (for maintenance) |
Multiple Apps on the Same Machine
One server, one IP — multiple apps. NGINX routes traffic based on domain name or URL path.
Internet
│
▼
NGINX (port 80 / 443) — one entry point
│
├──► myapp.com → React frontend (/var/www/myapp)
├──► api.myapp.com → Node.js :3000
└──► admin.myapp.com → Node.js :4000
Method 1: Subdomain-Based Routing
Each subdomain gets its own server block. DNS must point all subdomains to the same server IP.
# /etc/nginx/sites-available/frontend
server {
listen 80;
server_name myapp.com www.myapp.com;
root /var/www/frontend;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
# /etc/nginx/sites-available/api
server {
listen 80;
server_name api.myapp.com;
location / {
proxy_pass http://localhost: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;
}
}
# /etc/nginx/sites-available/admin
server {
listen 80;
server_name admin.myapp.com;
# Restrict to internal IPs only
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
location / {
proxy_pass http://localhost:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Enable all three:
sudo ln -s /etc/nginx/sites-available/frontend /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/admin /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Method 2: Path-Based Routing (Single Domain)
All traffic comes through one domain, routed by URL path.
# /etc/nginx/sites-available/myapp.com
server {
listen 80;
server_name myapp.com;
# React frontend — serve static files
location / {
root /var/www/frontend;
try_files $uri $uri/ /index.html;
}
# Node.js API
location /api/ {
proxy_pass http://localhost: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;
}
# Admin panel (different app)
location /admin/ {
proxy_pass http://localhost:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Static assets
location /assets/ {
alias /var/www/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
}
Full Example: React + Node.js on Same Server with SSL
# Redirect HTTP → HTTPS
server {
listen 80;
server_name myapp.com www.myapp.com;
return 301 https://$host$request_uri;
}
# Main HTTPS server
server {
listen 443 ssl http2;
server_name myapp.com www.myapp.com;
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Security headers
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
server_tokens off;
# React app (frontend)
location / {
root /var/www/frontend;
index index.html;
try_files $uri $uri/ /index.html;
}
# Node.js API
location /api/ {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
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 https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Static assets with aggressive caching
location ~* \.(js|css|png|jpg|ico|svg|woff2)$ {
root /var/www/frontend;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
}



Top comments (0)