Nginx
02 / 02

Nginx for Node.js Apps

Nginx for Node.js Apps

Nginx as a reverse proxy in front of Node.js (Next.js, Express, NestJS) handles SSL termination, static asset serving, load balancing, and protects the application server from direct internet exposure.

Reverse Proxy Configuration

# /etc/nginx/sites-enabled/nextjs-app.conf

server {
    listen 80;
    server_name myapp.com www.myapp.com;
    return 301 https://$host$request_uri;
}

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=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options DENY always;

    # Next.js static assets — serve directly (bypass Node.js entirely)
    location /_next/static/ {
        alias /home/deploy/myapp/.next/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Public folder
    location /public/ {
        alias /home/deploy/myapp/public/;
        expires 30d;
        add_header Cache-Control "public";
    }

    # Proxy everything else to Next.js app (port 3000)
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        # Required headers
        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 (Next.js HMR, Socket.io, etc.)
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Timeouts
        proxy_read_timeout    60s;
        proxy_connect_timeout 10s;
        proxy_send_timeout    60s;

        # Buffer settings
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
    }
}

Load Balancing

# upstream block — defines a pool of backend servers
upstream nodejs_cluster {
    # Default: round-robin (requests cycle through servers)
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;

    # keepalive — reuse connections to upstream (big performance win)
    keepalive 32;
}

# Least connections — send to server with fewest active connections
upstream nodejs_leastconn {
    least_conn;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

# IP hash — sticky sessions (same client -> same server)
upstream nodejs_sticky {
    ip_hash;
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

# Weighted — send 3x more traffic to first server
upstream nodejs_weighted {
    server 127.0.0.1:3000 weight=3;
    server 127.0.0.1:3001 weight=1;
}

# With health checks and fallback
upstream nodejs_resilient {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002 backup;   # only used if others are down

    # Mark server down after 3 failures in 30s, retry after 10s
    # (add to each server line:)
    # max_fails=3 fail_timeout=30s
}

server {
    listen 443 ssl http2;
    server_name myapp.com;
    # ... ssl config ...

    location / {
        proxy_pass http://nodejs_cluster;
        proxy_http_version 1.1;
        proxy_set_header Connection "";  # required for keepalive upstream
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Rate Limiting

# Define rate limit zones in http {} block (outside server {})
# $binary_remote_addr — compact IP representation (4 bytes)
# zone=name:size — shared memory; 1MB ~= 16,000 IPs
http {
    # General API: 10 requests/second per IP
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    # Auth endpoints: 5 requests/minute per IP (stricter)
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

    # Return 429 instead of default 503 when rate limited
    limit_req_status 429;
    limit_conn_status 429;
}

server {
    # ...

    # API routes: allow burst of 20 requests, process immediately (nodelay)
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://nodejs_cluster;
    }

    # Auth routes: strict, no burst
    location /api/auth/ {
        limit_req zone=login burst=3;
        proxy_pass http://nodejs_cluster;
    }

    # Limit concurrent connections per IP
    limit_conn_zone $binary_remote_addr zone=perip:10m;
    location /download/ {
        limit_conn perip 5;          # max 5 simultaneous downloads per IP
        limit_rate 500k;             # throttle bandwidth to 500KB/s
        proxy_pass http://nodejs_cluster;
    }
}

WebSocket Proxying

# WebSocket connections require the Upgrade and Connection headers
# Nginx needs to pass these through to upgrade HTTP to WS protocol

# map — dynamically set Connection header based on Upgrade presence
http {
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }
}

server {
    listen 443 ssl http2;
    server_name myapp.com;
    # ... ssl config ...

    # WebSocket endpoint (Socket.io, ws://, Next.js HMR)
    location /socket.io/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host       $host;
        proxy_set_header X-Real-IP  $remote_addr;
        proxy_cache_bypass $http_upgrade;

        # WS connections are long-lived — increase timeouts
        proxy_read_timeout 3600s;  # 1 hour
        proxy_send_timeout 3600s;
    }

    # Regular HTTP routes
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        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;
    }
}

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free