DEV Community

Cover image for What is nginx ?
Mayank Tamrkar
Mayank Tamrkar

Posted on

What is nginx ?

ngnix image

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 ✓
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Amazon Linux 2 (EC2)

sudo yum update -y
sudo amazon-linux-extras install nginx1 -y
sudo systemctl start nginx
sudo systemctl enable nginx
Enter fullscreen mode Exit fullscreen mode

Amazon Linux 2023

sudo dnf install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
Enter fullscreen mode Exit fullscreen mode

CentOS / RHEL

sudo yum install epel-release -y
sudo yum install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: Always run sudo nginx -t before 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/*;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# /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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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 to index.html lets 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

In your Express.js app, add this so req.ip returns the real client IP:

app.set('trust proxy', 1);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Use listen 443 ssl http2 — HTTP/2 allows multiplexing (multiple requests over one connection), which noticeably improves performance for sites with many assets.


ngnix 2

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)
Enter fullscreen mode Exit fullscreen mode
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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
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)

ngnix 3

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
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode
# /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;
    }
}
Enter fullscreen mode Exit fullscreen mode
# /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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)