Reverse Proxies (Nginx) · learncode.live

Why a Reverse Proxy?

Nginx sits in front of your Express app, handling concerns that Node.js shouldn’t handle directly:

Internet ───→ Nginx (port 80/443) ───→ Express (port 3000)
                │
                ├── SSL termination
                ├── Static file serving
                ├── Load balancing
                ├── Rate limiting
                ├── Compression
                ├── Security headers
                └── Request buffering
TaskNginxExpress
SSL/TLS❌ (use Nginx)
Static files✅ (fast)✅ (but slower)
Compression✅ (compression)
Rate limiting✅ (express-rate-limit)
Load balancing
Serving the app

Basic Nginx Reverse Proxy

Installation

# Ubuntu/Debian
sudo apt update
sudo apt install nginx

# CentOS/RHEL
sudo yum install nginx

Minimal Config

# /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header 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;
        proxy_cache_bypass $http_upgrade;
    }
}
# Enable site
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Important Nginx Proxy Headers

location / {
    proxy_pass http://127.0.0.1:3000;

    # Required for WebSocket support
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';

    # Required for correct client IP
    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;

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

    # Buffer sizes
    proxy_buffer_size 4k;
    proxy_buffers 8 4k;
    proxy_busy_buffers_size 8k;
}

Express Must Trust Proxy

// Essential when behind Nginx
app.set('trust proxy', 1);

// Now req.ip returns the real client IP
// req.protocol returns 'https' if Nginx sets X-Forwarded-Proto

SSL Termination with Let’s Encrypt

# Install Certbot
sudo apt install certbot python3-certbot-nginx

# Get SSL certificate
sudo certbot --nginx -d api.example.com

# Auto-renew (certbot adds a systemd timer)
sudo certbot renew --dry-run

Nginx SSL Config

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        # ... proxy headers
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name api.example.com;
    return 301 https://$server_name$request_uri;
}

Load Balancing Multiple Express Instances

upstream express_cluster {
    least_connections;        # Send to least busy instance
    # ip_hash;                # Stick sessions to same instance
    # random;                 # Random distribution

    server 127.0.0.1:3001 weight=3;
    server 127.0.0.1:3002 weight=2;
    server 127.0.0.1:3003 weight=1;
    server 127.0.0.1:3004 backup; # Only used if others are down
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    location / {
        proxy_pass http://express_cluster;
        # ... proxy headers
    }
}

Starting Express Instances with PM2

# Start 4 instances (one per CPU core)
pm2 start index.js -i 4 --name "myapp"

# Nginx automatically load balances across them

Serving Static Files via Nginx

Express is slow at serving static files. Let Nginx handle them:

server {
    listen 443 ssl http2;
    server_name example.com;

    # Serve static files directly (Nginx is much faster)
    location /static/ {
        alias /var/www/myapp/public/;
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    location /uploads/ {
        alias /var/www/myapp/uploads/;
        expires 7d;
        add_header Cache-Control "public";
    }

    # Everything else goes to Express
    location / {
        proxy_pass http://127.0.0.1:3000;
        # ... proxy headers
    }
}

Rate Limiting at Nginx Level

# Define a rate limit zone
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;

server {
    listen 443 ssl http2;
    server_name api.example.com;

    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;
        proxy_pass http://127.0.0.1:3000;
    }

    location /api/login {
        limit_req zone=login_limit:10m rate=5r/m;
        proxy_pass http://127.0.0.1:3000;
    }
}

Security Headers at Nginx Level

server {
    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # Content Security Policy
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;

    # HSTS (forces HTTPS)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}

Full Production Nginx Config

# /etc/nginx/sites-available/myapp
upstream express_app {
    least_connections;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    server 127.0.0.1:3003;
    server 127.0.0.1:3004;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # SSL
    ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Logging
    access_log /var/log/nginx/myapp-access.log;
    error_log /var/log/nginx/myapp-error.log;

    # Gzip
    gzip on;
    gzip_types application/json application/javascript text/css text/plain;
    gzip_min_length 1000;

    # Static files
    location /static/ {
        alias /var/www/myapp/public/;
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # API
    location /api/ {
        limit_req zone=api_limit:10m rate=100r/s burst=20;
        proxy_pass http://express_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header 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;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 90s;
    }

    # WebSocket (Socket.IO)
    location /socket.io/ {
        proxy_pass http://express_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 86400s;
    }

    # Default - proxy to Express
    location / {
        proxy_pass http://express_app;
        # ... proxy headers
    }
}

# HTTP → HTTPS redirect
server {
    listen 80;
    server_name api.example.com;
    return 301 https://$server_name$request_uri;
}

Troubleshooting

# Test config
sudo nginx -t

# Check status
sudo systemctl status nginx

# View logs
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log

# Reload after config change
sudo systemctl reload nginx

# Check what's listening on port 80
sudo ss -tlnp | grep :80

Key Takeaways

  • Nginx is a reverse proxy that sits between clients and Express
  • Handle SSL termination, static files, and gzip at the Nginx level
  • Use upstream blocks for load balancing across multiple Express instances
  • Set app.set('trust proxy', 1) in Express for correct client IPs
  • Use Let’s Encrypt (Certbot) for free SSL certificates
  • Add rate limiting and security headers at Nginx for performance
  • Configure WebSocket support with appropriate proxy headers and timeouts
  • Always redirect HTTP to HTTPS
Courses